From 644636c4ffdb0c36b47d1c70e3bfca32b127a477 Mon Sep 17 00:00:00 2001 From: Jorge Javier Araya Navarro Date: Wed, 14 Aug 2024 16:34:10 -0600 Subject: [PATCH 1/3] Migrate configuration to use schema fields --- README.md | 19 +- cmd/baton-okta/config.go | 78 +-- cmd/baton-okta/main.go | 28 +- go.mod | 8 +- go.sum | 15 +- .../allegro/bigcache/v3/.codecov.yml | 28 + .../github.com/allegro/bigcache/v3/.gitignore | 11 + vendor/github.com/allegro/bigcache/v3/LICENSE | 201 +++++++ .../github.com/allegro/bigcache/v3/README.md | 204 ++++++++ .../allegro/bigcache/v3/bigcache.go | 270 ++++++++++ .../github.com/allegro/bigcache/v3/bytes.go | 11 + .../allegro/bigcache/v3/bytes_appengine.go | 7 + .../github.com/allegro/bigcache/v3/clock.go | 14 + .../github.com/allegro/bigcache/v3/config.go | 97 ++++ .../allegro/bigcache/v3/encoding.go | 83 +++ .../bigcache/v3/entry_not_found_error.go | 8 + vendor/github.com/allegro/bigcache/v3/fnv.go | 28 + vendor/github.com/allegro/bigcache/v3/hash.go | 8 + .../allegro/bigcache/v3/iterator.go | 146 ++++++ .../github.com/allegro/bigcache/v3/logger.go | 30 ++ .../allegro/bigcache/v3/queue/bytes_queue.go | 269 ++++++++++ .../github.com/allegro/bigcache/v3/shard.go | 454 ++++++++++++++++ .../github.com/allegro/bigcache/v3/stats.go | 15 + .../github.com/allegro/bigcache/v3/utils.go | 23 + .../baton-sdk/internal/connector/connector.go | 8 + .../connector/v2/annotation_entitlement.pb.go | 164 ++++++ .../v2/annotation_entitlement.pb.validate.go | 169 ++++++ .../v2/annotation_external_ticket.pb.go | 148 ++++++ .../annotation_external_ticket.pb.validate.go | 140 +++++ .../pb/c1/connector/v2/annotation_grant.pb.go | 102 +++- .../v2/annotation_grant.pb.validate.go | 131 +++++ .../baton-sdk/pb/c1/connector/v2/ticket.pb.go | 88 ++-- .../pb/c1/connector/v2/ticket.pb.validate.go | 58 ++ .../conductorone/baton-sdk/pkg/cli/cli.go | 495 ------------------ .../baton-sdk/pkg/cli/commands.go | 330 ++++++++++++ .../conductorone/baton-sdk/pkg/cli/config.go | 146 ------ .../baton-sdk/pkg/cli/service_unix.go | 17 +- .../baton-sdk/pkg/cli/service_windows.go | 73 ++- .../baton-sdk/pkg/config/config.go | 228 ++++++++ .../pkg/connectorbuilder/connectorbuilder.go | 19 + .../baton-sdk/pkg/connectorrunner/runner.go | 14 +- .../pkg/connectorstore/connectorstore.go | 10 +- .../baton-sdk/pkg/dotc1z/assets.go | 5 - .../baton-sdk/pkg/dotc1z/c1file.go | 11 + .../baton-sdk/pkg/dotc1z/entitlements.go | 36 +- .../baton-sdk/pkg/dotc1z/grants.go | 46 +- .../baton-sdk/pkg/dotc1z/resouce_types.go | 31 +- .../baton-sdk/pkg/dotc1z/resources.go | 62 +-- .../baton-sdk/pkg/dotc1z/sql_helpers.go | 69 ++- .../baton-sdk/pkg/dotc1z/sync_runs.go | 49 +- .../pkg/field/default_relationships.go | 32 ++ .../baton-sdk/pkg/field/defaults.go | 94 ++++ .../baton-sdk/pkg/field/field_options.go | 42 ++ .../baton-sdk/pkg/field/fields.go | 149 ++++++ .../baton-sdk/pkg/field/relationships.go | 45 ++ .../baton-sdk/pkg/field/struct.go | 13 + .../baton-sdk/pkg/field/validation.go | 153 ++++++ .../baton-sdk/pkg/helpers/helpers.go | 107 +++- .../baton-sdk/pkg/metrics/instrumentor.go | 6 +- .../baton-sdk/pkg/metrics/otel.go | 136 +++-- .../baton-sdk/pkg/pagination/pagination.go | 6 +- .../conductorone/baton-sdk/pkg/sdk/version.go | 3 +- .../conductorone/baton-sdk/pkg/sync/expand.go | 408 --------------- .../baton-sdk/pkg/sync/expand/cycle.go | 213 ++++++++ .../baton-sdk/pkg/sync/expand/graph.go | 283 ++++++++++ .../baton-sdk/pkg/sync/expand/validate.go | 134 +++++ .../conductorone/baton-sdk/pkg/sync/state.go | 24 +- .../conductorone/baton-sdk/pkg/sync/syncer.go | 152 ++++-- .../pkg/tasks/c1api/create_ticket.go | 4 +- .../baton-sdk/pkg/tasks/c1api/full_sync.go | 32 +- .../baton-sdk/pkg/tasks/c1api/get_ticket.go | 5 +- .../pkg/tasks/c1api/list_ticket_schemas.go | 17 +- .../baton-sdk/pkg/tasks/c1api/manager.go | 6 +- .../baton-sdk/pkg/tasks/local/ticket.go | 34 +- .../pkg/types/ticket/custom_fields.go | 90 ++-- .../baton-sdk/pkg/uhttp/authcredentials.go | 3 +- .../baton-sdk/pkg/uhttp/client.go | 9 +- .../baton-sdk/pkg/uhttp/gocache.go | 170 ++++++ .../baton-sdk/pkg/uhttp/transport.go | 3 +- .../baton-sdk/pkg/uhttp/wrapper.go | 192 ++++++- .../deckarep/golang-set/v2/.gitignore | 23 + .../github.com/deckarep/golang-set/v2/LICENSE | 22 + .../deckarep/golang-set/v2/README.md | 181 +++++++ .../deckarep/golang-set/v2/iterator.go | 58 ++ .../deckarep/golang-set/v2/new_improved.jpeg | Bin 0 -> 120935 bytes .../github.com/deckarep/golang-set/v2/set.go | 255 +++++++++ .../deckarep/golang-set/v2/sorted.go | 42 ++ .../deckarep/golang-set/v2/threadsafe.go | 299 +++++++++++ .../deckarep/golang-set/v2/threadunsafe.go | 330 ++++++++++++ .../goqu/v9/dialect/sqlite3/sqlite3.go | 76 +++ vendor/modules.txt | 13 +- 91 files changed, 6960 insertions(+), 1608 deletions(-) create mode 100644 vendor/github.com/allegro/bigcache/v3/.codecov.yml create mode 100644 vendor/github.com/allegro/bigcache/v3/.gitignore create mode 100644 vendor/github.com/allegro/bigcache/v3/LICENSE create mode 100644 vendor/github.com/allegro/bigcache/v3/README.md create mode 100644 vendor/github.com/allegro/bigcache/v3/bigcache.go create mode 100644 vendor/github.com/allegro/bigcache/v3/bytes.go create mode 100644 vendor/github.com/allegro/bigcache/v3/bytes_appengine.go create mode 100644 vendor/github.com/allegro/bigcache/v3/clock.go create mode 100644 vendor/github.com/allegro/bigcache/v3/config.go create mode 100644 vendor/github.com/allegro/bigcache/v3/encoding.go create mode 100644 vendor/github.com/allegro/bigcache/v3/entry_not_found_error.go create mode 100644 vendor/github.com/allegro/bigcache/v3/fnv.go create mode 100644 vendor/github.com/allegro/bigcache/v3/hash.go create mode 100644 vendor/github.com/allegro/bigcache/v3/iterator.go create mode 100644 vendor/github.com/allegro/bigcache/v3/logger.go create mode 100644 vendor/github.com/allegro/bigcache/v3/queue/bytes_queue.go create mode 100644 vendor/github.com/allegro/bigcache/v3/shard.go create mode 100644 vendor/github.com/allegro/bigcache/v3/stats.go create mode 100644 vendor/github.com/allegro/bigcache/v3/utils.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_entitlement.pb.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_entitlement.pb.validate.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_external_ticket.pb.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_external_ticket.pb.validate.go delete mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/cli/cli.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/cli/commands.go delete mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/cli/config.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/config/config.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/field/default_relationships.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/field/defaults.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/field/field_options.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/field/fields.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/field/relationships.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/field/struct.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/field/validation.go delete mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/sync/expand.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/cycle.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/graph.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/validate.go create mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/uhttp/gocache.go create mode 100644 vendor/github.com/deckarep/golang-set/v2/.gitignore create mode 100644 vendor/github.com/deckarep/golang-set/v2/LICENSE create mode 100644 vendor/github.com/deckarep/golang-set/v2/README.md create mode 100644 vendor/github.com/deckarep/golang-set/v2/iterator.go create mode 100644 vendor/github.com/deckarep/golang-set/v2/new_improved.jpeg create mode 100644 vendor/github.com/deckarep/golang-set/v2/set.go create mode 100644 vendor/github.com/deckarep/golang-set/v2/sorted.go create mode 100644 vendor/github.com/deckarep/golang-set/v2/threadsafe.go create mode 100644 vendor/github.com/deckarep/golang-set/v2/threadunsafe.go create mode 100644 vendor/github.com/doug-martin/goqu/v9/dialect/sqlite3/sqlite3.go diff --git a/README.md b/README.md index b489b3ed..90e96cc8 100644 --- a/README.md +++ b/README.md @@ -76,19 +76,24 @@ Available Commands: help Help about any command Flags: - --api-token string The API token for the service account. ($BATON_API_TOKEN) + --api-token string The API token for the service account ($BATON_API_TOKEN) + --ciam Whether to run in CIAM mode or not. In CIAM mode, only roles and the users assigned to roles are synced ($BATON_CIAM) + --ciam-email-domains strings The email domains to use for CIAM mode. Any users that don't have an email address with one of the provided domains will be ignored, unless explicitly granted a role ($BATON_CIAM_EMAIL_DOMAINS) --client-id string The client ID used to authenticate with ConductorOne ($BATON_CLIENT_ID) --client-secret string The client secret used to authenticate with ConductorOne ($BATON_CLIENT_SECRET) - --domain string The URL for the Okta organization. ($BATON_DOMAIN) + --domain string required: The URL for the Okta organization ($BATON_DOMAIN) -f, --file string The path to the c1z file to sync with ($BATON_FILE) (default "sync.c1z") -h, --help help for baton-okta --log-format string The output format for logs: json, console ($BATON_LOG_FORMAT) (default "json") --log-level string The log level: debug, info, warn, error ($BATON_LOG_LEVEL) (default "info") - --okta-client-id string The Okta Client ID. ($BATON_OKTA_CLIENT_ID) - --okta-private-key string The Okta Private Key. This can be the whole private key or the path to the private key. ($BATON_OKTA_PRIVATE_KEY) - --okta-private-key-id string The Okta Private Key ID. ($BATON_OKTA_PRIVATE_KEY_ID) - -p, --provisioning This must be set in order for provisioning actions to be enabled. ($BATON_PROVISIONING) - --sync-inactive-apps Whether to sync inactive apps or not. ($BATON_SYNC_INACTIVE_APPS) (default true) + --okta-client-id string The Okta Client ID ($BATON_OKTA_CLIENT_ID) + --okta-private-key string The Okta Private Key. This can be the whole private key or the path to the private key ($BATON_OKTA_PRIVATE_KEY) + --okta-private-key-id string The Okta Private Key ID ($BATON_OKTA_PRIVATE_KEY_ID) + --okta-provisioning ($BATON_OKTA_PROVISIONING) + -p, --provisioning This must be set in order for provisioning actions to be enabled ($BATON_PROVISIONING) + --skip-full-sync This must be set to skip a full sync ($BATON_SKIP_FULL_SYNC) + --sync-inactive-apps Whether to sync inactive apps or not ($BATON_SYNC_INACTIVE_APPS) (default true) + --ticketing This must be set to enable ticketing support ($BATON_TICKETING) -v, --version version for baton-okta Use "baton-okta [command] --help" for more information about a command. diff --git a/cmd/baton-okta/config.go b/cmd/baton-okta/config.go index 6158a152..09d40f80 100644 --- a/cmd/baton-okta/config.go +++ b/cmd/baton-okta/config.go @@ -1,66 +1,28 @@ package main import ( - "context" - "fmt" - - "github.com/conductorone/baton-sdk/pkg/cli" - "github.com/spf13/cobra" + "github.com/conductorone/baton-sdk/pkg/field" ) -// config defines the external configuration required for the connector to run. -type Config struct { - cli.BaseConfig `mapstructure:",squash"` // Puts the base config options in the same place as the connector options - - Domain string `mapstructure:"domain"` - ApiToken string `mapstructure:"api-token"` - OktaClientId string `mapstructure:"okta-client-id"` - OktaPrivateKey string `mapstructure:"okta-private-key"` - OktaPrivateKeyId string `mapstructure:"okta-private-key-id"` - SyncInactiveApps bool `mapstructure:"sync-inactive-apps"` - OktaProvisioning bool `mapstructure:"provisioning"` - Ciam bool `mapstructure:"ciam"` - CiamEmailDomains []string `mapstructure:"ciam-email-domains"` -} - -// validateConfig is run after the configuration is loaded, and should return an error if it isn't valid. -func validateConfig(ctx context.Context, cfg *Config) error { - if cfg.Domain == "" { - return fmt.Errorf("domain is missing") - } - - if cfg.ApiToken == "" { - if cfg.OktaClientId == "" { - return fmt.Errorf("either api token or client id is required") - } else if cfg.OktaClientId != "" && cfg.OktaPrivateKey == "" || cfg.OktaPrivateKeyId == "" { - return fmt.Errorf("private key and private key id required") - } - - if cfg.OktaClientId == "" && cfg.OktaPrivateKey == "" && cfg.OktaPrivateKeyId == "" { - return fmt.Errorf("client id, private key and private key id required") - } - } - - if cfg.ApiToken != "" && cfg.OktaClientId != "" { - return fmt.Errorf("api token and client id cannot be provided simultaneously") - } +var ( + domain = field.StringField("domain", field.WithRequired(true), field.WithDescription("The URL for the Okta organization")) + apiToken = field.StringField("api-token", field.WithDescription("The API token for the service account")) + oktaClientId = field.StringField("okta-client-id", field.WithDescription("The Okta Client ID")) + oktaPrivateKeyId = field.StringField("okta-private-key-id", field.WithDescription("The Okta Private Key ID")) + oktaPrivateKey = field.StringField("okta-private-key", field.WithDescription("The Okta Private Key. This can be the whole private key or the path to the private key")) + syncInactivateApps = field.BoolField("sync-inactive-apps", field.WithDescription("Whether to sync inactive apps or not"), field.WithDefaultValue(true)) + oktaProvisioning = field.BoolField("okta-provisioning") + ciam = field.BoolField("ciam", field.WithDescription("Whether to run in CIAM mode or not. In CIAM mode, only roles and the users assigned to roles are synced")) + ciamEmailDomains = field.StringSliceField("ciam-email-domains", field.WithDescription("The email domains to use for CIAM mode. Any users that don't have an email address with one of the provided domains will be ignored, unless explicitly granted a role")) +) - return nil +var relationships = []field.SchemaFieldRelationship{ + field.FieldsRequiredTogether(oktaPrivateKeyId, oktaPrivateKey), + field.FieldsMutuallyExclusive(apiToken, oktaClientId), + field.FieldsAtLeastOneUsed(apiToken, oktaClientId), } -// cmdFlags sets the cmdFlags required for the connector. -func cmdFlags(cmd *cobra.Command) { - cmd.PersistentFlags().String("domain", "", "The URL for the Okta organization. ($BATON_DOMAIN)") - cmd.PersistentFlags().String("okta-client-id", "", "The Okta Client ID. ($BATON_OKTA_CLIENT_ID)") - cmd.PersistentFlags().String("okta-private-key", "", "The Okta Private Key. This can be the whole private key or the path to the private key. ($BATON_OKTA_PRIVATE_KEY)") - cmd.PersistentFlags().String("okta-private-key-id", "", "The Okta Private Key ID. ($BATON_OKTA_PRIVATE_KEY_ID)") - cmd.PersistentFlags().String("api-token", "", "The API token for the service account. ($BATON_API_TOKEN)") - cmd.PersistentFlags().Bool("sync-inactive-apps", true, "Whether to sync inactive apps or not. ($BATON_SYNC_INACTIVE_APPS)") - cmd.PersistentFlags().Bool("ciam", false, "Whether to run in CIAM mode or not. In CIAM mode, only roles and the users assigned to roles are synced. ($BATON_CIAM)") - cmd.PersistentFlags().StringSlice( - "ciam-email-domains", - nil, - "The email domains to use for CIAM mode. Any users that don't have an email address with one of the provided domains will be ignored,"+ - "unless explicitly granted a role. ($BATON_CIAM_EMAIL_DOMAIN)", - ) -} +var configuration = field.NewConfiguration([]field.SchemaField{ + domain, apiToken, oktaClientId, oktaPrivateKey, oktaPrivateKeyId, + syncInactivateApps, oktaProvisioning, ciam, ciamEmailDomains, +}, relationships...) diff --git a/cmd/baton-okta/main.go b/cmd/baton-okta/main.go index 9ea5c40c..a19cc7ac 100644 --- a/cmd/baton-okta/main.go +++ b/cmd/baton-okta/main.go @@ -5,13 +5,14 @@ import ( "fmt" "os" - "github.com/conductorone/baton-sdk/pkg/cli" "github.com/conductorone/baton-sdk/pkg/connectorbuilder" "github.com/conductorone/baton-sdk/pkg/types" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "github.com/spf13/viper" "go.uber.org/zap" "github.com/conductorone/baton-okta/pkg/connector" + configschema "github.com/conductorone/baton-sdk/pkg/config" ) var version = "dev" @@ -19,8 +20,7 @@ var version = "dev" func main() { ctx := context.Background() - cfg := &Config{} - cmd, err := cli.NewCmd(ctx, "baton-okta", cfg, validateConfig, getConnector) + _, cmd, err := configschema.DefineConfiguration(ctx, "baton-okta", getConnector, configuration) if err != nil { fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) @@ -28,8 +28,6 @@ func main() { cmd.Version = version - cmdFlags(cmd) - err = cmd.Execute() if err != nil { fmt.Fprintln(os.Stderr, err.Error()) @@ -37,19 +35,19 @@ func main() { } } -func getConnector(ctx context.Context, cfg *Config) (types.ConnectorServer, error) { +func getConnector(ctx context.Context, v *viper.Viper) (types.ConnectorServer, error) { l := ctxzap.Extract(ctx) ccfg := &connector.Config{ - Domain: cfg.Domain, - ApiToken: cfg.ApiToken, - OktaClientId: cfg.OktaClientId, - OktaPrivateKey: cfg.OktaPrivateKey, - OktaPrivateKeyId: cfg.OktaPrivateKeyId, - SyncInactiveApps: cfg.SyncInactiveApps, - OktaProvisioning: cfg.OktaProvisioning, - Ciam: cfg.Ciam, - CiamEmailDomains: cfg.CiamEmailDomains, + Domain: v.GetString("domain"), + ApiToken: v.GetString("api-token"), + OktaClientId: v.GetString("okta-client-id"), + OktaPrivateKey: v.GetString("okta-private-key"), + OktaPrivateKeyId: v.GetString("okta-private-key-id"), + SyncInactiveApps: v.GetBool("sync-inactive-apps"), + OktaProvisioning: v.GetBool("okta-provisioning"), + Ciam: v.GetBool("ciam"), + CiamEmailDomains: v.GetStringSlice("ciam-email-domains"), } cb, err := connector.New(ctx, ccfg) diff --git a/go.mod b/go.mod index 4d179974..c036d70f 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/conductorone/baton-okta go 1.22.2 require ( - github.com/conductorone/baton-sdk v0.1.43 + github.com/conductorone/baton-sdk v0.2.18 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/okta/okta-sdk-golang/v2 v2.20.0 - github.com/spf13/cobra v1.8.0 + github.com/spf13/viper v1.19.0 go.uber.org/zap v1.27.0 google.golang.org/protobuf v1.34.1 ) @@ -15,6 +15,7 @@ require ( filippo.io/age v1.1.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect + github.com/allegro/bigcache/v3 v3.1.0 // indirect github.com/aws/aws-sdk-go-v2 v1.27.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect github.com/aws/aws-sdk-go-v2/config v1.27.17 // indirect @@ -36,6 +37,7 @@ require ( github.com/aws/smithy-go v1.20.2 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/doug-martin/goqu/v9 v9.19.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect @@ -68,8 +70,8 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cobra v1.8.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.19.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.8.0 // indirect diff --git a/go.sum b/go.sum index 79dc8a7e..49c93b58 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0 github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk= +github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I= github.com/aws/aws-sdk-go-v2 v1.27.1 h1:xypCL2owhog46iFxBKKpBcw+bPTX/RJzwNj8uSilENw= github.com/aws/aws-sdk-go-v2 v1.27.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to= @@ -54,13 +56,15 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/conductorone/baton-sdk v0.1.43 h1:fEOsR/Os66GhA/FpXpldjcm7NIyUZ0TNCBkWmqtalrs= -github.com/conductorone/baton-sdk v0.1.43/go.mod h1:CxHwuTWhrX2w5yEuAxoRWIUofimnnnM9ancsZPvTko8= +github.com/conductorone/baton-sdk v0.2.18 h1:nnEtw0qKl1s6zbDbQxlq6beA6Az+gwYGSycMG9X7fgg= +github.com/conductorone/baton-sdk v0.2.18/go.mod h1:hmd/Oz3DPIKD+9QmkusZaA18ZoiinnTDdrxh2skcdUc= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/doug-martin/goqu/v9 v9.19.0 h1:PD7t1X3tRcUiSdc5TEyOFKujZA5gs3VSA7wxSvBx7qo= github.com/doug-martin/goqu/v9 v9.19.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ= @@ -141,6 +145,7 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA= github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -220,8 +225,14 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0 h1:/jlt1Y8gXWiHG9FBx6cJaIC5hYx5Fe64nC8w5Cylt/0= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0/go.mod h1:bmToOGOBZ4hA9ghphIc1PAf66VA8KOtsuy3+ScStG20= go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= +go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= +go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI= +go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= diff --git a/vendor/github.com/allegro/bigcache/v3/.codecov.yml b/vendor/github.com/allegro/bigcache/v3/.codecov.yml new file mode 100644 index 00000000..d8c862ec --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/.codecov.yml @@ -0,0 +1,28 @@ +--- +codecov: + require_ci_to_pass: true +comment: + behavior: default + layout: reach, diff, flags, files, footer + require_base: false + require_changes: false + require_head: true +coverage: + precision: 2 + range: + - 70 + - 100 + round: down + status: + changes: false + patch: true + project: true +parsers: + gcov: + branch_detection: + conditional: true + loop: true + macro: false + method: false + javascript: + enable_partials: false diff --git a/vendor/github.com/allegro/bigcache/v3/.gitignore b/vendor/github.com/allegro/bigcache/v3/.gitignore new file mode 100644 index 00000000..78869152 --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/.gitignore @@ -0,0 +1,11 @@ +.idea +.DS_Store +/server/server.exe +/server/server +/server/server_dar* +/server/server_fre* +/server/server_win* +/server/server_net* +/server/server_ope* +/server/server_lin* +CHANGELOG.md diff --git a/vendor/github.com/allegro/bigcache/v3/LICENSE b/vendor/github.com/allegro/bigcache/v3/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/allegro/bigcache/v3/README.md b/vendor/github.com/allegro/bigcache/v3/README.md new file mode 100644 index 00000000..253749be --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/README.md @@ -0,0 +1,204 @@ +# BigCache [![Build Status](https://github.com/allegro/bigcache/workflows/build/badge.svg)](https://github.com/allegro/bigcache/actions?query=workflow%3Abuild) [![Coverage Status](https://coveralls.io/repos/github/allegro/bigcache/badge.svg?branch=master)](https://coveralls.io/github/allegro/bigcache?branch=master) [![GoDoc](https://godoc.org/github.com/allegro/bigcache/v3?status.svg)](https://godoc.org/github.com/allegro/bigcache/v3) [![Go Report Card](https://goreportcard.com/badge/github.com/allegro/bigcache/v3)](https://goreportcard.com/report/github.com/allegro/bigcache/v3) + +Fast, concurrent, evicting in-memory cache written to keep big number of entries without impact on performance. +BigCache keeps entries on heap but omits GC for them. To achieve that, operations on byte slices take place, +therefore entries (de)serialization in front of the cache will be needed in most use cases. + +Requires Go 1.12 or newer. + +## Usage + +### Simple initialization + +```go +import ( + "fmt" + "context" + "github.com/allegro/bigcache/v3" +) + +cache, _ := bigcache.New(context.Background(), bigcache.DefaultConfig(10 * time.Minute)) + +cache.Set("my-unique-key", []byte("value")) + +entry, _ := cache.Get("my-unique-key") +fmt.Println(string(entry)) +``` + +### Custom initialization + +When cache load can be predicted in advance then it is better to use custom initialization because additional memory +allocation can be avoided in that way. + +```go +import ( + "log" + + "github.com/allegro/bigcache/v3" +) + +config := bigcache.Config { + // number of shards (must be a power of 2) + Shards: 1024, + + // time after which entry can be evicted + LifeWindow: 10 * time.Minute, + + // Interval between removing expired entries (clean up). + // If set to <= 0 then no action is performed. + // Setting to < 1 second is counterproductive — bigcache has a one second resolution. + CleanWindow: 5 * time.Minute, + + // rps * lifeWindow, used only in initial memory allocation + MaxEntriesInWindow: 1000 * 10 * 60, + + // max entry size in bytes, used only in initial memory allocation + MaxEntrySize: 500, + + // prints information about additional memory allocation + Verbose: true, + + // cache will not allocate more memory than this limit, value in MB + // if value is reached then the oldest entries can be overridden for the new ones + // 0 value means no size limit + HardMaxCacheSize: 8192, + + // callback fired when the oldest entry is removed because of its expiration time or no space left + // for the new entry, or because delete was called. A bitmask representing the reason will be returned. + // Default value is nil which means no callback and it prevents from unwrapping the oldest entry. + OnRemove: nil, + + // OnRemoveWithReason is a callback fired when the oldest entry is removed because of its expiration time or no space left + // for the new entry, or because delete was called. A constant representing the reason will be passed through. + // Default value is nil which means no callback and it prevents from unwrapping the oldest entry. + // Ignored if OnRemove is specified. + OnRemoveWithReason: nil, + } + +cache, initErr := bigcache.New(context.Background(), config) +if initErr != nil { + log.Fatal(initErr) +} + +cache.Set("my-unique-key", []byte("value")) + +if entry, err := cache.Get("my-unique-key"); err == nil { + fmt.Println(string(entry)) +} +``` + +### `LifeWindow` & `CleanWindow` + +1. `LifeWindow` is a time. After that time, an entry can be called dead but not deleted. + +2. `CleanWindow` is a time. After that time, all the dead entries will be deleted, but not the entries that still have life. + +## [Benchmarks](https://github.com/allegro/bigcache-bench) + +Three caches were compared: bigcache, [freecache](https://github.com/coocood/freecache) and map. +Benchmark tests were made using an +i7-6700K CPU @ 4.00GHz with 32GB of RAM on Ubuntu 18.04 LTS (5.2.12-050212-generic). + +Benchmarks source code can be found [here](https://github.com/allegro/bigcache-bench) + +### Writes and reads + +```bash +go version +go version go1.13 linux/amd64 + +go test -bench=. -benchmem -benchtime=4s ./... -timeout 30m +goos: linux +goarch: amd64 +pkg: github.com/allegro/bigcache/v3/caches_bench +BenchmarkMapSet-8 12999889 376 ns/op 199 B/op 3 allocs/op +BenchmarkConcurrentMapSet-8 4355726 1275 ns/op 337 B/op 8 allocs/op +BenchmarkFreeCacheSet-8 11068976 703 ns/op 328 B/op 2 allocs/op +BenchmarkBigCacheSet-8 10183717 478 ns/op 304 B/op 2 allocs/op +BenchmarkMapGet-8 16536015 324 ns/op 23 B/op 1 allocs/op +BenchmarkConcurrentMapGet-8 13165708 401 ns/op 24 B/op 2 allocs/op +BenchmarkFreeCacheGet-8 10137682 690 ns/op 136 B/op 2 allocs/op +BenchmarkBigCacheGet-8 11423854 450 ns/op 152 B/op 4 allocs/op +BenchmarkBigCacheSetParallel-8 34233472 148 ns/op 317 B/op 3 allocs/op +BenchmarkFreeCacheSetParallel-8 34222654 268 ns/op 350 B/op 3 allocs/op +BenchmarkConcurrentMapSetParallel-8 19635688 240 ns/op 200 B/op 6 allocs/op +BenchmarkBigCacheGetParallel-8 60547064 86.1 ns/op 152 B/op 4 allocs/op +BenchmarkFreeCacheGetParallel-8 50701280 147 ns/op 136 B/op 3 allocs/op +BenchmarkConcurrentMapGetParallel-8 27353288 175 ns/op 24 B/op 2 allocs/op +PASS +ok github.com/allegro/bigcache/v3/caches_bench 256.257s +``` + +Writes and reads in bigcache are faster than in freecache. +Writes to map are the slowest. + +### GC pause time + +```bash +go version +go version go1.13 linux/amd64 + +go run caches_gc_overhead_comparison.go + +Number of entries: 20000000 +GC pause for bigcache: 1.506077ms +GC pause for freecache: 5.594416ms +GC pause for map: 9.347015ms +``` + +``` +go version +go version go1.13 linux/arm64 + +go run caches_gc_overhead_comparison.go +Number of entries: 20000000 +GC pause for bigcache: 22.382827ms +GC pause for freecache: 41.264651ms +GC pause for map: 72.236853ms +``` + +Test shows how long are the GC pauses for caches filled with 20mln of entries. +Bigcache and freecache have very similar GC pause time. + +### Memory usage + +You may encounter system memory reporting what appears to be an exponential increase, however this is expected behaviour. Go runtime allocates memory in chunks or 'spans' and will inform the OS when they are no longer required by changing their state to 'idle'. The 'spans' will remain part of the process resource usage until the OS needs to repurpose the address. Further reading available [here](https://utcc.utoronto.ca/~cks/space/blog/programming/GoNoMemoryFreeing). + +## How it works + +BigCache relies on optimization presented in 1.5 version of Go ([issue-9477](https://github.com/golang/go/issues/9477)). +This optimization states that if map without pointers in keys and values is used then GC will omit its content. +Therefore BigCache uses `map[uint64]uint32` where keys are hashed and values are offsets of entries. + +Entries are kept in byte slices, to omit GC again. +Byte slices size can grow to gigabytes without impact on performance +because GC will only see single pointer to it. + +### Collisions + +BigCache does not handle collisions. When new item is inserted and it's hash collides with previously stored item, new item overwrites previously stored value. + +## Bigcache vs Freecache + +Both caches provide the same core features but they reduce GC overhead in different ways. +Bigcache relies on `map[uint64]uint32`, freecache implements its own mapping built on +slices to reduce number of pointers. + +Results from benchmark tests are presented above. +One of the advantage of bigcache over freecache is that you don’t need to know +the size of the cache in advance, because when bigcache is full, +it can allocate additional memory for new entries instead of +overwriting existing ones as freecache does currently. +However hard max size in bigcache also can be set, check [HardMaxCacheSize](https://godoc.org/github.com/allegro/bigcache#Config). + +## HTTP Server + +This package also includes an easily deployable HTTP implementation of BigCache, which can be found in the [server](/server) package. + +## More + +Bigcache genesis is described in allegro.tech blog post: [writing a very fast cache service in Go](http://allegro.tech/2016/03/writing-fast-cache-service-in-go.html) + +## License + +BigCache is released under the Apache 2.0 license (see [LICENSE](LICENSE)) diff --git a/vendor/github.com/allegro/bigcache/v3/bigcache.go b/vendor/github.com/allegro/bigcache/v3/bigcache.go new file mode 100644 index 00000000..17e2aca7 --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/bigcache.go @@ -0,0 +1,270 @@ +package bigcache + +import ( + "context" + "fmt" + "time" +) + +const ( + minimumEntriesInShard = 10 // Minimum number of entries in single shard +) + +// BigCache is fast, concurrent, evicting cache created to keep big number of entries without impact on performance. +// It keeps entries on heap but omits GC for them. To achieve that, operations take place on byte arrays, +// therefore entries (de)serialization in front of the cache will be needed in most use cases. +type BigCache struct { + shards []*cacheShard + lifeWindow uint64 + clock clock + hash Hasher + config Config + shardMask uint64 + close chan struct{} +} + +// Response will contain metadata about the entry for which GetWithInfo(key) was called +type Response struct { + EntryStatus RemoveReason +} + +// RemoveReason is a value used to signal to the user why a particular key was removed in the OnRemove callback. +type RemoveReason uint32 + +const ( + // Expired means the key is past its LifeWindow. + Expired = RemoveReason(1) + // NoSpace means the key is the oldest and the cache size was at its maximum when Set was called, or the + // entry exceeded the maximum shard size. + NoSpace = RemoveReason(2) + // Deleted means Delete was called and this key was removed as a result. + Deleted = RemoveReason(3) +) + +// New initialize new instance of BigCache +func New(ctx context.Context, config Config) (*BigCache, error) { + return newBigCache(ctx, config, &systemClock{}) +} + +// NewBigCache initialize new instance of BigCache +// +// Deprecated: NewBigCache is deprecated, please use New(ctx, config) instead, +// New takes in context and can gracefully +// shutdown with context cancellations +func NewBigCache(config Config) (*BigCache, error) { + return newBigCache(context.Background(), config, &systemClock{}) +} + +func newBigCache(ctx context.Context, config Config, clock clock) (*BigCache, error) { + if !isPowerOfTwo(config.Shards) { + return nil, fmt.Errorf("Shards number must be power of two") + } + if config.MaxEntrySize < 0 { + return nil, fmt.Errorf("MaxEntrySize must be >= 0") + } + if config.MaxEntriesInWindow < 0 { + return nil, fmt.Errorf("MaxEntriesInWindow must be >= 0") + } + if config.HardMaxCacheSize < 0 { + return nil, fmt.Errorf("HardMaxCacheSize must be >= 0") + } + + if config.Hasher == nil { + config.Hasher = newDefaultHasher() + } + + cache := &BigCache{ + shards: make([]*cacheShard, config.Shards), + lifeWindow: uint64(config.LifeWindow.Seconds()), + clock: clock, + hash: config.Hasher, + config: config, + shardMask: uint64(config.Shards - 1), + close: make(chan struct{}), + } + + var onRemove func(wrappedEntry []byte, reason RemoveReason) + if config.OnRemoveWithMetadata != nil { + onRemove = cache.providedOnRemoveWithMetadata + } else if config.OnRemove != nil { + onRemove = cache.providedOnRemove + } else if config.OnRemoveWithReason != nil { + onRemove = cache.providedOnRemoveWithReason + } else { + onRemove = cache.notProvidedOnRemove + } + + for i := 0; i < config.Shards; i++ { + cache.shards[i] = initNewShard(config, onRemove, clock) + } + + if config.CleanWindow > 0 { + go func() { + ticker := time.NewTicker(config.CleanWindow) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + fmt.Println("ctx done, shutting down bigcache cleanup routine") + return + case t := <-ticker.C: + cache.cleanUp(uint64(t.Unix())) + case <-cache.close: + return + } + } + }() + } + + return cache, nil +} + +// Close is used to signal a shutdown of the cache when you are done with it. +// This allows the cleaning goroutines to exit and ensures references are not +// kept to the cache preventing GC of the entire cache. +func (c *BigCache) Close() error { + close(c.close) + return nil +} + +// Get reads entry for the key. +// It returns an ErrEntryNotFound when +// no entry exists for the given key. +func (c *BigCache) Get(key string) ([]byte, error) { + hashedKey := c.hash.Sum64(key) + shard := c.getShard(hashedKey) + return shard.get(key, hashedKey) +} + +// GetWithInfo reads entry for the key with Response info. +// It returns an ErrEntryNotFound when +// no entry exists for the given key. +func (c *BigCache) GetWithInfo(key string) ([]byte, Response, error) { + hashedKey := c.hash.Sum64(key) + shard := c.getShard(hashedKey) + return shard.getWithInfo(key, hashedKey) +} + +// Set saves entry under the key +func (c *BigCache) Set(key string, entry []byte) error { + hashedKey := c.hash.Sum64(key) + shard := c.getShard(hashedKey) + return shard.set(key, hashedKey, entry) +} + +// Append appends entry under the key if key exists, otherwise +// it will set the key (same behaviour as Set()). With Append() you can +// concatenate multiple entries under the same key in an lock optimized way. +func (c *BigCache) Append(key string, entry []byte) error { + hashedKey := c.hash.Sum64(key) + shard := c.getShard(hashedKey) + return shard.append(key, hashedKey, entry) +} + +// Delete removes the key +func (c *BigCache) Delete(key string) error { + hashedKey := c.hash.Sum64(key) + shard := c.getShard(hashedKey) + return shard.del(hashedKey) +} + +// Reset empties all cache shards +func (c *BigCache) Reset() error { + for _, shard := range c.shards { + shard.reset(c.config) + } + return nil +} + +// ResetStats resets cache stats +func (c *BigCache) ResetStats() error { + for _, shard := range c.shards { + shard.resetStats() + } + return nil +} + +// Len computes number of entries in cache +func (c *BigCache) Len() int { + var len int + for _, shard := range c.shards { + len += shard.len() + } + return len +} + +// Capacity returns amount of bytes store in the cache. +func (c *BigCache) Capacity() int { + var len int + for _, shard := range c.shards { + len += shard.capacity() + } + return len +} + +// Stats returns cache's statistics +func (c *BigCache) Stats() Stats { + var s Stats + for _, shard := range c.shards { + tmp := shard.getStats() + s.Hits += tmp.Hits + s.Misses += tmp.Misses + s.DelHits += tmp.DelHits + s.DelMisses += tmp.DelMisses + s.Collisions += tmp.Collisions + } + return s +} + +// KeyMetadata returns number of times a cached resource was requested. +func (c *BigCache) KeyMetadata(key string) Metadata { + hashedKey := c.hash.Sum64(key) + shard := c.getShard(hashedKey) + return shard.getKeyMetadataWithLock(hashedKey) +} + +// Iterator returns iterator function to iterate over EntryInfo's from whole cache. +func (c *BigCache) Iterator() *EntryInfoIterator { + return newIterator(c) +} + +func (c *BigCache) onEvict(oldestEntry []byte, currentTimestamp uint64, evict func(reason RemoveReason) error) bool { + oldestTimestamp := readTimestampFromEntry(oldestEntry) + if currentTimestamp < oldestTimestamp { + return false + } + if currentTimestamp-oldestTimestamp > c.lifeWindow { + evict(Expired) + return true + } + return false +} + +func (c *BigCache) cleanUp(currentTimestamp uint64) { + for _, shard := range c.shards { + shard.cleanUp(currentTimestamp) + } +} + +func (c *BigCache) getShard(hashedKey uint64) (shard *cacheShard) { + return c.shards[hashedKey&c.shardMask] +} + +func (c *BigCache) providedOnRemove(wrappedEntry []byte, reason RemoveReason) { + c.config.OnRemove(readKeyFromEntry(wrappedEntry), readEntry(wrappedEntry)) +} + +func (c *BigCache) providedOnRemoveWithReason(wrappedEntry []byte, reason RemoveReason) { + if c.config.onRemoveFilter == 0 || (1< 0 { + c.config.OnRemoveWithReason(readKeyFromEntry(wrappedEntry), readEntry(wrappedEntry), reason) + } +} + +func (c *BigCache) notProvidedOnRemove(wrappedEntry []byte, reason RemoveReason) { +} + +func (c *BigCache) providedOnRemoveWithMetadata(wrappedEntry []byte, reason RemoveReason) { + hashedKey := c.hash.Sum64(readKeyFromEntry(wrappedEntry)) + shard := c.getShard(hashedKey) + c.config.OnRemoveWithMetadata(readKeyFromEntry(wrappedEntry), readEntry(wrappedEntry), shard.getKeyMetadata(hashedKey)) +} diff --git a/vendor/github.com/allegro/bigcache/v3/bytes.go b/vendor/github.com/allegro/bigcache/v3/bytes.go new file mode 100644 index 00000000..2bf2e9a9 --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/bytes.go @@ -0,0 +1,11 @@ +// +build !appengine + +package bigcache + +import ( + "unsafe" +) + +func bytesToString(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} diff --git a/vendor/github.com/allegro/bigcache/v3/bytes_appengine.go b/vendor/github.com/allegro/bigcache/v3/bytes_appengine.go new file mode 100644 index 00000000..3892f3b5 --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/bytes_appengine.go @@ -0,0 +1,7 @@ +// +build appengine + +package bigcache + +func bytesToString(b []byte) string { + return string(b) +} diff --git a/vendor/github.com/allegro/bigcache/v3/clock.go b/vendor/github.com/allegro/bigcache/v3/clock.go new file mode 100644 index 00000000..195d01af --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/clock.go @@ -0,0 +1,14 @@ +package bigcache + +import "time" + +type clock interface { + Epoch() int64 +} + +type systemClock struct { +} + +func (c systemClock) Epoch() int64 { + return time.Now().Unix() +} diff --git a/vendor/github.com/allegro/bigcache/v3/config.go b/vendor/github.com/allegro/bigcache/v3/config.go new file mode 100644 index 00000000..63a4e9b1 --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/config.go @@ -0,0 +1,97 @@ +package bigcache + +import "time" + +// Config for BigCache +type Config struct { + // Number of cache shards, value must be a power of two + Shards int + // Time after which entry can be evicted + LifeWindow time.Duration + // Interval between removing expired entries (clean up). + // If set to <= 0 then no action is performed. Setting to < 1 second is counterproductive — bigcache has a one second resolution. + CleanWindow time.Duration + // Max number of entries in life window. Used only to calculate initial size for cache shards. + // When proper value is set then additional memory allocation does not occur. + MaxEntriesInWindow int + // Max size of entry in bytes. Used only to calculate initial size for cache shards. + MaxEntrySize int + // StatsEnabled if true calculate the number of times a cached resource was requested. + StatsEnabled bool + // Verbose mode prints information about new memory allocation + Verbose bool + // Hasher used to map between string keys and unsigned 64bit integers, by default fnv64 hashing is used. + Hasher Hasher + // HardMaxCacheSize is a limit for BytesQueue size in MB. + // It can protect application from consuming all available memory on machine, therefore from running OOM Killer. + // Default value is 0 which means unlimited size. When the limit is higher than 0 and reached then + // the oldest entries are overridden for the new ones. The max memory consumption will be bigger than + // HardMaxCacheSize due to Shards' s additional memory. Every Shard consumes additional memory for map of keys + // and statistics (map[uint64]uint32) the size of this map is equal to number of entries in + // cache ~ 2×(64+32)×n bits + overhead or map itself. + HardMaxCacheSize int + // OnRemove is a callback fired when the oldest entry is removed because of its expiration time or no space left + // for the new entry, or because delete was called. + // Default value is nil which means no callback and it prevents from unwrapping the oldest entry. + // ignored if OnRemoveWithMetadata is specified. + OnRemove func(key string, entry []byte) + // OnRemoveWithMetadata is a callback fired when the oldest entry is removed because of its expiration time or no space left + // for the new entry, or because delete was called. A structure representing details about that specific entry. + // Default value is nil which means no callback and it prevents from unwrapping the oldest entry. + OnRemoveWithMetadata func(key string, entry []byte, keyMetadata Metadata) + // OnRemoveWithReason is a callback fired when the oldest entry is removed because of its expiration time or no space left + // for the new entry, or because delete was called. A constant representing the reason will be passed through. + // Default value is nil which means no callback and it prevents from unwrapping the oldest entry. + // Ignored if OnRemove is specified. + OnRemoveWithReason func(key string, entry []byte, reason RemoveReason) + + onRemoveFilter int + + // Logger is a logging interface and used in combination with `Verbose` + // Defaults to `DefaultLogger()` + Logger Logger +} + +// DefaultConfig initializes config with default values. +// When load for BigCache can be predicted in advance then it is better to use custom config. +func DefaultConfig(eviction time.Duration) Config { + return Config{ + Shards: 1024, + LifeWindow: eviction, + CleanWindow: 1 * time.Second, + MaxEntriesInWindow: 1000 * 10 * 60, + MaxEntrySize: 500, + StatsEnabled: false, + Verbose: true, + Hasher: newDefaultHasher(), + HardMaxCacheSize: 0, + Logger: DefaultLogger(), + } +} + +// initialShardSize computes initial shard size +func (c Config) initialShardSize() int { + return max(c.MaxEntriesInWindow/c.Shards, minimumEntriesInShard) +} + +// maximumShardSizeInBytes computes maximum shard size in bytes +func (c Config) maximumShardSizeInBytes() int { + maxShardSize := 0 + + if c.HardMaxCacheSize > 0 { + maxShardSize = convertMBToBytes(c.HardMaxCacheSize) / c.Shards + } + + return maxShardSize +} + +// OnRemoveFilterSet sets which remove reasons will trigger a call to OnRemoveWithReason. +// Filtering out reasons prevents bigcache from unwrapping them, which saves cpu. +func (c Config) OnRemoveFilterSet(reasons ...RemoveReason) Config { + c.onRemoveFilter = 0 + for i := range reasons { + c.onRemoveFilter |= 1 << uint(reasons[i]) + } + + return c +} diff --git a/vendor/github.com/allegro/bigcache/v3/encoding.go b/vendor/github.com/allegro/bigcache/v3/encoding.go new file mode 100644 index 00000000..fc861251 --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/encoding.go @@ -0,0 +1,83 @@ +package bigcache + +import ( + "encoding/binary" +) + +const ( + timestampSizeInBytes = 8 // Number of bytes used for timestamp + hashSizeInBytes = 8 // Number of bytes used for hash + keySizeInBytes = 2 // Number of bytes used for size of entry key + headersSizeInBytes = timestampSizeInBytes + hashSizeInBytes + keySizeInBytes // Number of bytes used for all headers +) + +func wrapEntry(timestamp uint64, hash uint64, key string, entry []byte, buffer *[]byte) []byte { + keyLength := len(key) + blobLength := len(entry) + headersSizeInBytes + keyLength + + if blobLength > len(*buffer) { + *buffer = make([]byte, blobLength) + } + blob := *buffer + + binary.LittleEndian.PutUint64(blob, timestamp) + binary.LittleEndian.PutUint64(blob[timestampSizeInBytes:], hash) + binary.LittleEndian.PutUint16(blob[timestampSizeInBytes+hashSizeInBytes:], uint16(keyLength)) + copy(blob[headersSizeInBytes:], key) + copy(blob[headersSizeInBytes+keyLength:], entry) + + return blob[:blobLength] +} + +func appendToWrappedEntry(timestamp uint64, wrappedEntry []byte, entry []byte, buffer *[]byte) []byte { + blobLength := len(wrappedEntry) + len(entry) + if blobLength > len(*buffer) { + *buffer = make([]byte, blobLength) + } + + blob := *buffer + + binary.LittleEndian.PutUint64(blob, timestamp) + copy(blob[timestampSizeInBytes:], wrappedEntry[timestampSizeInBytes:]) + copy(blob[len(wrappedEntry):], entry) + + return blob[:blobLength] +} + +func readEntry(data []byte) []byte { + length := binary.LittleEndian.Uint16(data[timestampSizeInBytes+hashSizeInBytes:]) + + // copy on read + dst := make([]byte, len(data)-int(headersSizeInBytes+length)) + copy(dst, data[headersSizeInBytes+length:]) + + return dst +} + +func readTimestampFromEntry(data []byte) uint64 { + return binary.LittleEndian.Uint64(data) +} + +func readKeyFromEntry(data []byte) string { + length := binary.LittleEndian.Uint16(data[timestampSizeInBytes+hashSizeInBytes:]) + + // copy on read + dst := make([]byte, length) + copy(dst, data[headersSizeInBytes:headersSizeInBytes+length]) + + return bytesToString(dst) +} + +func compareKeyFromEntry(data []byte, key string) bool { + length := binary.LittleEndian.Uint16(data[timestampSizeInBytes+hashSizeInBytes:]) + + return bytesToString(data[headersSizeInBytes:headersSizeInBytes+length]) == key +} + +func readHashFromEntry(data []byte) uint64 { + return binary.LittleEndian.Uint64(data[timestampSizeInBytes:]) +} + +func resetKeyFromEntry(data []byte) { + binary.LittleEndian.PutUint64(data[timestampSizeInBytes:], 0) +} diff --git a/vendor/github.com/allegro/bigcache/v3/entry_not_found_error.go b/vendor/github.com/allegro/bigcache/v3/entry_not_found_error.go new file mode 100644 index 00000000..8993384d --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/entry_not_found_error.go @@ -0,0 +1,8 @@ +package bigcache + +import "errors" + +var ( + // ErrEntryNotFound is an error type struct which is returned when entry was not found for provided key + ErrEntryNotFound = errors.New("Entry not found") +) diff --git a/vendor/github.com/allegro/bigcache/v3/fnv.go b/vendor/github.com/allegro/bigcache/v3/fnv.go new file mode 100644 index 00000000..188c9aa6 --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/fnv.go @@ -0,0 +1,28 @@ +package bigcache + +// newDefaultHasher returns a new 64-bit FNV-1a Hasher which makes no memory allocations. +// Its Sum64 method will lay the value out in big-endian byte order. +// See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function +func newDefaultHasher() Hasher { + return fnv64a{} +} + +type fnv64a struct{} + +const ( + // offset64 FNVa offset basis. See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function#FNV-1a_hash + offset64 = 14695981039346656037 + // prime64 FNVa prime value. See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function#FNV-1a_hash + prime64 = 1099511628211 +) + +// Sum64 gets the string and returns its uint64 hash value. +func (f fnv64a) Sum64(key string) uint64 { + var hash uint64 = offset64 + for i := 0; i < len(key); i++ { + hash ^= uint64(key[i]) + hash *= prime64 + } + + return hash +} diff --git a/vendor/github.com/allegro/bigcache/v3/hash.go b/vendor/github.com/allegro/bigcache/v3/hash.go new file mode 100644 index 00000000..5f8ade77 --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/hash.go @@ -0,0 +1,8 @@ +package bigcache + +// Hasher is responsible for generating unsigned, 64 bit hash of provided string. Hasher should minimize collisions +// (generating same hash for different strings) and while performance is also important fast functions are preferable (i.e. +// you can use FarmHash family). +type Hasher interface { + Sum64(string) uint64 +} diff --git a/vendor/github.com/allegro/bigcache/v3/iterator.go b/vendor/github.com/allegro/bigcache/v3/iterator.go new file mode 100644 index 00000000..db2a2ef4 --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/iterator.go @@ -0,0 +1,146 @@ +package bigcache + +import ( + "sync" +) + +type iteratorError string + +func (e iteratorError) Error() string { + return string(e) +} + +// ErrInvalidIteratorState is reported when iterator is in invalid state +const ErrInvalidIteratorState = iteratorError("Iterator is in invalid state. Use SetNext() to move to next position") + +// ErrCannotRetrieveEntry is reported when entry cannot be retrieved from underlying +const ErrCannotRetrieveEntry = iteratorError("Could not retrieve entry from cache") + +var emptyEntryInfo = EntryInfo{} + +// EntryInfo holds informations about entry in the cache +type EntryInfo struct { + timestamp uint64 + hash uint64 + key string + value []byte + err error +} + +// Key returns entry's underlying key +func (e EntryInfo) Key() string { + return e.key +} + +// Hash returns entry's hash value +func (e EntryInfo) Hash() uint64 { + return e.hash +} + +// Timestamp returns entry's timestamp (time of insertion) +func (e EntryInfo) Timestamp() uint64 { + return e.timestamp +} + +// Value returns entry's underlying value +func (e EntryInfo) Value() []byte { + return e.value +} + +// EntryInfoIterator allows to iterate over entries in the cache +type EntryInfoIterator struct { + mutex sync.Mutex + cache *BigCache + currentShard int + currentIndex int + currentEntryInfo EntryInfo + elements []uint64 + elementsCount int + valid bool +} + +// SetNext moves to next element and returns true if it exists. +func (it *EntryInfoIterator) SetNext() bool { + it.mutex.Lock() + + it.valid = false + it.currentIndex++ + + if it.elementsCount > it.currentIndex { + it.valid = true + + empty := it.setCurrentEntry() + it.mutex.Unlock() + + if empty { + return it.SetNext() + } + return true + } + + for i := it.currentShard + 1; i < it.cache.config.Shards; i++ { + it.elements, it.elementsCount = it.cache.shards[i].copyHashedKeys() + + // Non empty shard - stick with it + if it.elementsCount > 0 { + it.currentIndex = 0 + it.currentShard = i + it.valid = true + + empty := it.setCurrentEntry() + it.mutex.Unlock() + + if empty { + return it.SetNext() + } + return true + } + } + it.mutex.Unlock() + return false +} + +func (it *EntryInfoIterator) setCurrentEntry() bool { + var entryNotFound = false + entry, err := it.cache.shards[it.currentShard].getEntry(it.elements[it.currentIndex]) + + if err == ErrEntryNotFound { + it.currentEntryInfo = emptyEntryInfo + entryNotFound = true + } else if err != nil { + it.currentEntryInfo = EntryInfo{ + err: err, + } + } else { + it.currentEntryInfo = EntryInfo{ + timestamp: readTimestampFromEntry(entry), + hash: readHashFromEntry(entry), + key: readKeyFromEntry(entry), + value: readEntry(entry), + err: err, + } + } + + return entryNotFound +} + +func newIterator(cache *BigCache) *EntryInfoIterator { + elements, count := cache.shards[0].copyHashedKeys() + + return &EntryInfoIterator{ + cache: cache, + currentShard: 0, + currentIndex: -1, + elements: elements, + elementsCount: count, + } +} + +// Value returns current value from the iterator +func (it *EntryInfoIterator) Value() (EntryInfo, error) { + if !it.valid { + return emptyEntryInfo, ErrInvalidIteratorState + } + + return it.currentEntryInfo, it.currentEntryInfo.err +} diff --git a/vendor/github.com/allegro/bigcache/v3/logger.go b/vendor/github.com/allegro/bigcache/v3/logger.go new file mode 100644 index 00000000..50e84abc --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/logger.go @@ -0,0 +1,30 @@ +package bigcache + +import ( + "log" + "os" +) + +// Logger is invoked when `Config.Verbose=true` +type Logger interface { + Printf(format string, v ...interface{}) +} + +// this is a safeguard, breaking on compile time in case +// `log.Logger` does not adhere to our `Logger` interface. +// see https://golang.org/doc/faq#guarantee_satisfies_interface +var _ Logger = &log.Logger{} + +// DefaultLogger returns a `Logger` implementation +// backed by stdlib's log +func DefaultLogger() *log.Logger { + return log.New(os.Stdout, "", log.LstdFlags) +} + +func newLogger(custom Logger) Logger { + if custom != nil { + return custom + } + + return DefaultLogger() +} diff --git a/vendor/github.com/allegro/bigcache/v3/queue/bytes_queue.go b/vendor/github.com/allegro/bigcache/v3/queue/bytes_queue.go new file mode 100644 index 00000000..3ef0f6d9 --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/queue/bytes_queue.go @@ -0,0 +1,269 @@ +package queue + +import ( + "encoding/binary" + "log" + "time" +) + +const ( + // Number of bytes to encode 0 in uvarint format + minimumHeaderSize = 17 // 1 byte blobsize + timestampSizeInBytes + hashSizeInBytes + // Bytes before left margin are not used. Zero index means element does not exist in queue, useful while reading slice from index + leftMarginIndex = 1 +) + +var ( + errEmptyQueue = &queueError{"Empty queue"} + errInvalidIndex = &queueError{"Index must be greater than zero. Invalid index."} + errIndexOutOfBounds = &queueError{"Index out of range"} +) + +// BytesQueue is a non-thread safe queue type of fifo based on bytes array. +// For every push operation index of entry is returned. It can be used to read the entry later +type BytesQueue struct { + full bool + array []byte + capacity int + maxCapacity int + head int + tail int + count int + rightMargin int + headerBuffer []byte + verbose bool +} + +type queueError struct { + message string +} + +// getNeededSize returns the number of bytes an entry of length need in the queue +func getNeededSize(length int) int { + var header int + switch { + case length < 127: // 1<<7-1 + header = 1 + case length < 16382: // 1<<14-2 + header = 2 + case length < 2097149: // 1<<21 -3 + header = 3 + case length < 268435452: // 1<<28 -4 + header = 4 + default: + header = 5 + } + + return length + header +} + +// NewBytesQueue initialize new bytes queue. +// capacity is used in bytes array allocation +// When verbose flag is set then information about memory allocation are printed +func NewBytesQueue(capacity int, maxCapacity int, verbose bool) *BytesQueue { + return &BytesQueue{ + array: make([]byte, capacity), + capacity: capacity, + maxCapacity: maxCapacity, + headerBuffer: make([]byte, binary.MaxVarintLen32), + tail: leftMarginIndex, + head: leftMarginIndex, + rightMargin: leftMarginIndex, + verbose: verbose, + } +} + +// Reset removes all entries from queue +func (q *BytesQueue) Reset() { + // Just reset indexes + q.tail = leftMarginIndex + q.head = leftMarginIndex + q.rightMargin = leftMarginIndex + q.count = 0 + q.full = false +} + +// Push copies entry at the end of queue and moves tail pointer. Allocates more space if needed. +// Returns index for pushed data or error if maximum size queue limit is reached. +func (q *BytesQueue) Push(data []byte) (int, error) { + neededSize := getNeededSize(len(data)) + + if !q.canInsertAfterTail(neededSize) { + if q.canInsertBeforeHead(neededSize) { + q.tail = leftMarginIndex + } else if q.capacity+neededSize >= q.maxCapacity && q.maxCapacity > 0 { + return -1, &queueError{"Full queue. Maximum size limit reached."} + } else { + q.allocateAdditionalMemory(neededSize) + } + } + + index := q.tail + + q.push(data, neededSize) + + return index, nil +} + +func (q *BytesQueue) allocateAdditionalMemory(minimum int) { + start := time.Now() + if q.capacity < minimum { + q.capacity += minimum + } + q.capacity = q.capacity * 2 + if q.capacity > q.maxCapacity && q.maxCapacity > 0 { + q.capacity = q.maxCapacity + } + + oldArray := q.array + q.array = make([]byte, q.capacity) + + if leftMarginIndex != q.rightMargin { + copy(q.array, oldArray[:q.rightMargin]) + + if q.tail <= q.head { + if q.tail != q.head { + // created slice is slightly larger then need but this is fine after only the needed bytes are copied + q.push(make([]byte, q.head-q.tail), q.head-q.tail) + } + + q.head = leftMarginIndex + q.tail = q.rightMargin + } + } + + q.full = false + + if q.verbose { + log.Printf("Allocated new queue in %s; Capacity: %d \n", time.Since(start), q.capacity) + } +} + +func (q *BytesQueue) push(data []byte, len int) { + headerEntrySize := binary.PutUvarint(q.headerBuffer, uint64(len)) + q.copy(q.headerBuffer, headerEntrySize) + + q.copy(data, len-headerEntrySize) + + if q.tail > q.head { + q.rightMargin = q.tail + } + if q.tail == q.head { + q.full = true + } + + q.count++ +} + +func (q *BytesQueue) copy(data []byte, len int) { + q.tail += copy(q.array[q.tail:], data[:len]) +} + +// Pop reads the oldest entry from queue and moves head pointer to the next one +func (q *BytesQueue) Pop() ([]byte, error) { + data, blockSize, err := q.peek(q.head) + if err != nil { + return nil, err + } + + q.head += blockSize + q.count-- + + if q.head == q.rightMargin { + q.head = leftMarginIndex + if q.tail == q.rightMargin { + q.tail = leftMarginIndex + } + q.rightMargin = q.tail + } + + q.full = false + + return data, nil +} + +// Peek reads the oldest entry from list without moving head pointer +func (q *BytesQueue) Peek() ([]byte, error) { + data, _, err := q.peek(q.head) + return data, err +} + +// Get reads entry from index +func (q *BytesQueue) Get(index int) ([]byte, error) { + data, _, err := q.peek(index) + return data, err +} + +// CheckGet checks if an entry can be read from index +func (q *BytesQueue) CheckGet(index int) error { + return q.peekCheckErr(index) +} + +// Capacity returns number of allocated bytes for queue +func (q *BytesQueue) Capacity() int { + return q.capacity +} + +// Len returns number of entries kept in queue +func (q *BytesQueue) Len() int { + return q.count +} + +// Error returns error message +func (e *queueError) Error() string { + return e.message +} + +// peekCheckErr is identical to peek, but does not actually return any data +func (q *BytesQueue) peekCheckErr(index int) error { + + if q.count == 0 { + return errEmptyQueue + } + + if index <= 0 { + return errInvalidIndex + } + + if index >= len(q.array) { + return errIndexOutOfBounds + } + return nil +} + +// peek returns the data from index and the number of bytes to encode the length of the data in uvarint format +func (q *BytesQueue) peek(index int) ([]byte, int, error) { + err := q.peekCheckErr(index) + if err != nil { + return nil, 0, err + } + + blockSize, n := binary.Uvarint(q.array[index:]) + return q.array[index+n : index+int(blockSize)], int(blockSize), nil +} + +// canInsertAfterTail returns true if it's possible to insert an entry of size of need after the tail of the queue +func (q *BytesQueue) canInsertAfterTail(need int) bool { + if q.full { + return false + } + if q.tail >= q.head { + return q.capacity-q.tail >= need + } + // 1. there is exactly need bytes between head and tail, so we do not need + // to reserve extra space for a potential empty entry when realloc this queue + // 2. still have unused space between tail and head, then we must reserve + // at least headerEntrySize bytes so we can put an empty entry + return q.head-q.tail == need || q.head-q.tail >= need+minimumHeaderSize +} + +// canInsertBeforeHead returns true if it's possible to insert an entry of size of need before the head of the queue +func (q *BytesQueue) canInsertBeforeHead(need int) bool { + if q.full { + return false + } + if q.tail >= q.head { + return q.head-leftMarginIndex == need || q.head-leftMarginIndex >= need+minimumHeaderSize + } + return q.head-q.tail == need || q.head-q.tail >= need+minimumHeaderSize +} diff --git a/vendor/github.com/allegro/bigcache/v3/shard.go b/vendor/github.com/allegro/bigcache/v3/shard.go new file mode 100644 index 00000000..e80a759a --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/shard.go @@ -0,0 +1,454 @@ +package bigcache + +import ( + "fmt" + "sync" + "sync/atomic" + + "github.com/allegro/bigcache/v3/queue" +) + +type onRemoveCallback func(wrappedEntry []byte, reason RemoveReason) + +// Metadata contains information of a specific entry +type Metadata struct { + RequestCount uint32 +} + +type cacheShard struct { + hashmap map[uint64]uint32 + entries queue.BytesQueue + lock sync.RWMutex + entryBuffer []byte + onRemove onRemoveCallback + + isVerbose bool + statsEnabled bool + logger Logger + clock clock + lifeWindow uint64 + + hashmapStats map[uint64]uint32 + stats Stats + cleanEnabled bool +} + +func (s *cacheShard) getWithInfo(key string, hashedKey uint64) (entry []byte, resp Response, err error) { + currentTime := uint64(s.clock.Epoch()) + s.lock.RLock() + wrappedEntry, err := s.getWrappedEntry(hashedKey) + if err != nil { + s.lock.RUnlock() + return nil, resp, err + } + if entryKey := readKeyFromEntry(wrappedEntry); key != entryKey { + s.lock.RUnlock() + s.collision() + if s.isVerbose { + s.logger.Printf("Collision detected. Both %q and %q have the same hash %x", key, entryKey, hashedKey) + } + return nil, resp, ErrEntryNotFound + } + + entry = readEntry(wrappedEntry) + if s.isExpired(wrappedEntry, currentTime) { + resp.EntryStatus = Expired + } + s.lock.RUnlock() + s.hit(hashedKey) + return entry, resp, nil +} + +func (s *cacheShard) get(key string, hashedKey uint64) ([]byte, error) { + s.lock.RLock() + wrappedEntry, err := s.getWrappedEntry(hashedKey) + if err != nil { + s.lock.RUnlock() + return nil, err + } + if entryKey := readKeyFromEntry(wrappedEntry); key != entryKey { + s.lock.RUnlock() + s.collision() + if s.isVerbose { + s.logger.Printf("Collision detected. Both %q and %q have the same hash %x", key, entryKey, hashedKey) + } + return nil, ErrEntryNotFound + } + entry := readEntry(wrappedEntry) + s.lock.RUnlock() + s.hit(hashedKey) + + return entry, nil +} + +func (s *cacheShard) getWrappedEntry(hashedKey uint64) ([]byte, error) { + itemIndex := s.hashmap[hashedKey] + + if itemIndex == 0 { + s.miss() + return nil, ErrEntryNotFound + } + + wrappedEntry, err := s.entries.Get(int(itemIndex)) + if err != nil { + s.miss() + return nil, err + } + + return wrappedEntry, err +} + +func (s *cacheShard) getValidWrapEntry(key string, hashedKey uint64) ([]byte, error) { + wrappedEntry, err := s.getWrappedEntry(hashedKey) + if err != nil { + return nil, err + } + + if !compareKeyFromEntry(wrappedEntry, key) { + s.collision() + if s.isVerbose { + s.logger.Printf("Collision detected. Both %q and %q have the same hash %x", key, readKeyFromEntry(wrappedEntry), hashedKey) + } + + return nil, ErrEntryNotFound + } + s.hitWithoutLock(hashedKey) + + return wrappedEntry, nil +} + +func (s *cacheShard) set(key string, hashedKey uint64, entry []byte) error { + currentTimestamp := uint64(s.clock.Epoch()) + + s.lock.Lock() + + if previousIndex := s.hashmap[hashedKey]; previousIndex != 0 { + if previousEntry, err := s.entries.Get(int(previousIndex)); err == nil { + resetKeyFromEntry(previousEntry) + //remove hashkey + delete(s.hashmap, hashedKey) + } + } + + if !s.cleanEnabled { + if oldestEntry, err := s.entries.Peek(); err == nil { + s.onEvict(oldestEntry, currentTimestamp, s.removeOldestEntry) + } + } + + w := wrapEntry(currentTimestamp, hashedKey, key, entry, &s.entryBuffer) + + for { + if index, err := s.entries.Push(w); err == nil { + s.hashmap[hashedKey] = uint32(index) + s.lock.Unlock() + return nil + } + if s.removeOldestEntry(NoSpace) != nil { + s.lock.Unlock() + return fmt.Errorf("entry is bigger than max shard size") + } + } +} + +func (s *cacheShard) addNewWithoutLock(key string, hashedKey uint64, entry []byte) error { + currentTimestamp := uint64(s.clock.Epoch()) + + if !s.cleanEnabled { + if oldestEntry, err := s.entries.Peek(); err == nil { + s.onEvict(oldestEntry, currentTimestamp, s.removeOldestEntry) + } + } + + w := wrapEntry(currentTimestamp, hashedKey, key, entry, &s.entryBuffer) + + for { + if index, err := s.entries.Push(w); err == nil { + s.hashmap[hashedKey] = uint32(index) + return nil + } + if s.removeOldestEntry(NoSpace) != nil { + return fmt.Errorf("entry is bigger than max shard size") + } + } +} + +func (s *cacheShard) setWrappedEntryWithoutLock(currentTimestamp uint64, w []byte, hashedKey uint64) error { + if previousIndex := s.hashmap[hashedKey]; previousIndex != 0 { + if previousEntry, err := s.entries.Get(int(previousIndex)); err == nil { + resetKeyFromEntry(previousEntry) + } + } + + if !s.cleanEnabled { + if oldestEntry, err := s.entries.Peek(); err == nil { + s.onEvict(oldestEntry, currentTimestamp, s.removeOldestEntry) + } + } + + for { + if index, err := s.entries.Push(w); err == nil { + s.hashmap[hashedKey] = uint32(index) + return nil + } + if s.removeOldestEntry(NoSpace) != nil { + return fmt.Errorf("entry is bigger than max shard size") + } + } +} + +func (s *cacheShard) append(key string, hashedKey uint64, entry []byte) error { + s.lock.Lock() + wrappedEntry, err := s.getValidWrapEntry(key, hashedKey) + + if err == ErrEntryNotFound { + err = s.addNewWithoutLock(key, hashedKey, entry) + s.lock.Unlock() + return err + } + if err != nil { + s.lock.Unlock() + return err + } + + currentTimestamp := uint64(s.clock.Epoch()) + + w := appendToWrappedEntry(currentTimestamp, wrappedEntry, entry, &s.entryBuffer) + + err = s.setWrappedEntryWithoutLock(currentTimestamp, w, hashedKey) + s.lock.Unlock() + + return err +} + +func (s *cacheShard) del(hashedKey uint64) error { + // Optimistic pre-check using only readlock + s.lock.RLock() + { + itemIndex := s.hashmap[hashedKey] + + if itemIndex == 0 { + s.lock.RUnlock() + s.delmiss() + return ErrEntryNotFound + } + + if err := s.entries.CheckGet(int(itemIndex)); err != nil { + s.lock.RUnlock() + s.delmiss() + return err + } + } + s.lock.RUnlock() + + s.lock.Lock() + { + // After obtaining the writelock, we need to read the same again, + // since the data delivered earlier may be stale now + itemIndex := s.hashmap[hashedKey] + + if itemIndex == 0 { + s.lock.Unlock() + s.delmiss() + return ErrEntryNotFound + } + + wrappedEntry, err := s.entries.Get(int(itemIndex)) + if err != nil { + s.lock.Unlock() + s.delmiss() + return err + } + + delete(s.hashmap, hashedKey) + s.onRemove(wrappedEntry, Deleted) + if s.statsEnabled { + delete(s.hashmapStats, hashedKey) + } + resetKeyFromEntry(wrappedEntry) + } + s.lock.Unlock() + + s.delhit() + return nil +} + +func (s *cacheShard) onEvict(oldestEntry []byte, currentTimestamp uint64, evict func(reason RemoveReason) error) bool { + if s.isExpired(oldestEntry, currentTimestamp) { + evict(Expired) + return true + } + return false +} + +func (s *cacheShard) isExpired(oldestEntry []byte, currentTimestamp uint64) bool { + oldestTimestamp := readTimestampFromEntry(oldestEntry) + if currentTimestamp <= oldestTimestamp { // if currentTimestamp < oldestTimestamp, the result will out of uint64 limits; + return false + } + return currentTimestamp-oldestTimestamp > s.lifeWindow +} + +func (s *cacheShard) cleanUp(currentTimestamp uint64) { + s.lock.Lock() + for { + if oldestEntry, err := s.entries.Peek(); err != nil { + break + } else if evicted := s.onEvict(oldestEntry, currentTimestamp, s.removeOldestEntry); !evicted { + break + } + } + s.lock.Unlock() +} + +func (s *cacheShard) getEntry(hashedKey uint64) ([]byte, error) { + s.lock.RLock() + + entry, err := s.getWrappedEntry(hashedKey) + // copy entry + newEntry := make([]byte, len(entry)) + copy(newEntry, entry) + + s.lock.RUnlock() + + return newEntry, err +} + +func (s *cacheShard) copyHashedKeys() (keys []uint64, next int) { + s.lock.RLock() + keys = make([]uint64, len(s.hashmap)) + + for key := range s.hashmap { + keys[next] = key + next++ + } + + s.lock.RUnlock() + return keys, next +} + +func (s *cacheShard) removeOldestEntry(reason RemoveReason) error { + oldest, err := s.entries.Pop() + if err == nil { + hash := readHashFromEntry(oldest) + if hash == 0 { + // entry has been explicitly deleted with resetKeyFromEntry, ignore + return nil + } + delete(s.hashmap, hash) + s.onRemove(oldest, reason) + if s.statsEnabled { + delete(s.hashmapStats, hash) + } + return nil + } + return err +} + +func (s *cacheShard) reset(config Config) { + s.lock.Lock() + s.hashmap = make(map[uint64]uint32, config.initialShardSize()) + s.entryBuffer = make([]byte, config.MaxEntrySize+headersSizeInBytes) + s.entries.Reset() + s.lock.Unlock() +} + +func (s *cacheShard) resetStats() { + s.lock.Lock() + s.stats = Stats{} + s.lock.Unlock() +} + +func (s *cacheShard) len() int { + s.lock.RLock() + res := len(s.hashmap) + s.lock.RUnlock() + return res +} + +func (s *cacheShard) capacity() int { + s.lock.RLock() + res := s.entries.Capacity() + s.lock.RUnlock() + return res +} + +func (s *cacheShard) getStats() Stats { + var stats = Stats{ + Hits: atomic.LoadInt64(&s.stats.Hits), + Misses: atomic.LoadInt64(&s.stats.Misses), + DelHits: atomic.LoadInt64(&s.stats.DelHits), + DelMisses: atomic.LoadInt64(&s.stats.DelMisses), + Collisions: atomic.LoadInt64(&s.stats.Collisions), + } + return stats +} + +func (s *cacheShard) getKeyMetadataWithLock(key uint64) Metadata { + s.lock.RLock() + c := s.hashmapStats[key] + s.lock.RUnlock() + return Metadata{ + RequestCount: c, + } +} + +func (s *cacheShard) getKeyMetadata(key uint64) Metadata { + return Metadata{ + RequestCount: s.hashmapStats[key], + } +} + +func (s *cacheShard) hit(key uint64) { + atomic.AddInt64(&s.stats.Hits, 1) + if s.statsEnabled { + s.lock.Lock() + s.hashmapStats[key]++ + s.lock.Unlock() + } +} + +func (s *cacheShard) hitWithoutLock(key uint64) { + atomic.AddInt64(&s.stats.Hits, 1) + if s.statsEnabled { + s.hashmapStats[key]++ + } +} + +func (s *cacheShard) miss() { + atomic.AddInt64(&s.stats.Misses, 1) +} + +func (s *cacheShard) delhit() { + atomic.AddInt64(&s.stats.DelHits, 1) +} + +func (s *cacheShard) delmiss() { + atomic.AddInt64(&s.stats.DelMisses, 1) +} + +func (s *cacheShard) collision() { + atomic.AddInt64(&s.stats.Collisions, 1) +} + +func initNewShard(config Config, callback onRemoveCallback, clock clock) *cacheShard { + bytesQueueInitialCapacity := config.initialShardSize() * config.MaxEntrySize + maximumShardSizeInBytes := config.maximumShardSizeInBytes() + if maximumShardSizeInBytes > 0 && bytesQueueInitialCapacity > maximumShardSizeInBytes { + bytesQueueInitialCapacity = maximumShardSizeInBytes + } + return &cacheShard{ + hashmap: make(map[uint64]uint32, config.initialShardSize()), + hashmapStats: make(map[uint64]uint32, config.initialShardSize()), + entries: *queue.NewBytesQueue(bytesQueueInitialCapacity, maximumShardSizeInBytes, config.Verbose), + entryBuffer: make([]byte, config.MaxEntrySize+headersSizeInBytes), + onRemove: callback, + + isVerbose: config.Verbose, + logger: newLogger(config.Logger), + clock: clock, + lifeWindow: uint64(config.LifeWindow.Seconds()), + statsEnabled: config.StatsEnabled, + cleanEnabled: config.CleanWindow > 0, + } +} diff --git a/vendor/github.com/allegro/bigcache/v3/stats.go b/vendor/github.com/allegro/bigcache/v3/stats.go new file mode 100644 index 00000000..07157132 --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/stats.go @@ -0,0 +1,15 @@ +package bigcache + +// Stats stores cache statistics +type Stats struct { + // Hits is a number of successfully found keys + Hits int64 `json:"hits"` + // Misses is a number of not found keys + Misses int64 `json:"misses"` + // DelHits is a number of successfully deleted keys + DelHits int64 `json:"delete_hits"` + // DelMisses is a number of not deleted keys + DelMisses int64 `json:"delete_misses"` + // Collisions is a number of happened key-collisions + Collisions int64 `json:"collisions"` +} diff --git a/vendor/github.com/allegro/bigcache/v3/utils.go b/vendor/github.com/allegro/bigcache/v3/utils.go new file mode 100644 index 00000000..2b6ac4f1 --- /dev/null +++ b/vendor/github.com/allegro/bigcache/v3/utils.go @@ -0,0 +1,23 @@ +package bigcache + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func convertMBToBytes(value int) int { + return value * 1024 * 1024 +} + +func isPowerOfTwo(number int) bool { + return (number != 0) && (number&(number-1)) == 0 +} diff --git a/vendor/github.com/conductorone/baton-sdk/internal/connector/connector.go b/vendor/github.com/conductorone/baton-sdk/internal/connector/connector.go index 10f2a05b..52c2759f 100644 --- a/vendor/github.com/conductorone/baton-sdk/internal/connector/connector.go +++ b/vendor/github.com/conductorone/baton-sdk/internal/connector/connector.go @@ -57,6 +57,7 @@ type wrapper struct { conn *grpc.ClientConn provisioningEnabled bool ticketingEnabled bool + fullSyncDisabled bool rateLimiter ratelimitV1.RateLimiterServiceServer rlCfg *ratelimitV1.RateLimiterConfig @@ -95,6 +96,13 @@ func WithProvisioningEnabled() Option { } } +func WithFullSyncDisabled() Option { + return func(ctx context.Context, w *wrapper) error { + w.fullSyncDisabled = true + return nil + } +} + func WithTicketingEnabled() Option { return func(ctx context.Context, w *wrapper) error { w.ticketingEnabled = true diff --git a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_entitlement.pb.go b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_entitlement.pb.go new file mode 100644 index 00000000..7e536816 --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_entitlement.pb.go @@ -0,0 +1,164 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: c1/connector/v2/annotation_entitlement.proto + +package v2 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type EntitlementImmutable struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SourceId string `protobuf:"bytes,1,opt,name=source_id,json=sourceId,proto3" json:"source_id,omitempty"` + Metadata *structpb.Struct `protobuf:"bytes,2,opt,name=metadata,proto3" json:"metadata,omitempty"` +} + +func (x *EntitlementImmutable) Reset() { + *x = EntitlementImmutable{} + if protoimpl.UnsafeEnabled { + mi := &file_c1_connector_v2_annotation_entitlement_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EntitlementImmutable) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EntitlementImmutable) ProtoMessage() {} + +func (x *EntitlementImmutable) ProtoReflect() protoreflect.Message { + mi := &file_c1_connector_v2_annotation_entitlement_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EntitlementImmutable.ProtoReflect.Descriptor instead. +func (*EntitlementImmutable) Descriptor() ([]byte, []int) { + return file_c1_connector_v2_annotation_entitlement_proto_rawDescGZIP(), []int{0} +} + +func (x *EntitlementImmutable) GetSourceId() string { + if x != nil { + return x.SourceId + } + return "" +} + +func (x *EntitlementImmutable) GetMetadata() *structpb.Struct { + if x != nil { + return x.Metadata + } + return nil +} + +var File_c1_connector_v2_annotation_entitlement_proto protoreflect.FileDescriptor + +var file_c1_connector_v2_annotation_entitlement_proto_rawDesc = []byte{ + 0x0a, 0x2c, 0x63, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2f, 0x76, + 0x32, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, + 0x63, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x32, 0x1a, + 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x68, 0x0a, + 0x14, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x6d, 0x6d, 0x75, + 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x49, 0x64, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x64, 0x75, 0x63, 0x74, 0x6f, 0x72, 0x6f, + 0x6e, 0x65, 0x2f, 0x62, 0x61, 0x74, 0x6f, 0x6e, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x2f, + 0x63, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2f, 0x76, 0x32, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_c1_connector_v2_annotation_entitlement_proto_rawDescOnce sync.Once + file_c1_connector_v2_annotation_entitlement_proto_rawDescData = file_c1_connector_v2_annotation_entitlement_proto_rawDesc +) + +func file_c1_connector_v2_annotation_entitlement_proto_rawDescGZIP() []byte { + file_c1_connector_v2_annotation_entitlement_proto_rawDescOnce.Do(func() { + file_c1_connector_v2_annotation_entitlement_proto_rawDescData = protoimpl.X.CompressGZIP(file_c1_connector_v2_annotation_entitlement_proto_rawDescData) + }) + return file_c1_connector_v2_annotation_entitlement_proto_rawDescData +} + +var file_c1_connector_v2_annotation_entitlement_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_c1_connector_v2_annotation_entitlement_proto_goTypes = []interface{}{ + (*EntitlementImmutable)(nil), // 0: c1.connector.v2.EntitlementImmutable + (*structpb.Struct)(nil), // 1: google.protobuf.Struct +} +var file_c1_connector_v2_annotation_entitlement_proto_depIdxs = []int32{ + 1, // 0: c1.connector.v2.EntitlementImmutable.metadata:type_name -> google.protobuf.Struct + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_c1_connector_v2_annotation_entitlement_proto_init() } +func file_c1_connector_v2_annotation_entitlement_proto_init() { + if File_c1_connector_v2_annotation_entitlement_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_c1_connector_v2_annotation_entitlement_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EntitlementImmutable); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_c1_connector_v2_annotation_entitlement_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_c1_connector_v2_annotation_entitlement_proto_goTypes, + DependencyIndexes: file_c1_connector_v2_annotation_entitlement_proto_depIdxs, + MessageInfos: file_c1_connector_v2_annotation_entitlement_proto_msgTypes, + }.Build() + File_c1_connector_v2_annotation_entitlement_proto = out.File + file_c1_connector_v2_annotation_entitlement_proto_rawDesc = nil + file_c1_connector_v2_annotation_entitlement_proto_goTypes = nil + file_c1_connector_v2_annotation_entitlement_proto_depIdxs = nil +} diff --git a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_entitlement.pb.validate.go b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_entitlement.pb.validate.go new file mode 100644 index 00000000..189bf53e --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_entitlement.pb.validate.go @@ -0,0 +1,169 @@ +// Code generated by protoc-gen-validate. DO NOT EDIT. +// source: c1/connector/v2/annotation_entitlement.proto + +package v2 + +import ( + "bytes" + "errors" + "fmt" + "net" + "net/mail" + "net/url" + "regexp" + "sort" + "strings" + "time" + "unicode/utf8" + + "google.golang.org/protobuf/types/known/anypb" +) + +// ensure the imports are used +var ( + _ = bytes.MinRead + _ = errors.New("") + _ = fmt.Print + _ = utf8.UTFMax + _ = (*regexp.Regexp)(nil) + _ = (*strings.Reader)(nil) + _ = net.IPv4len + _ = time.Duration(0) + _ = (*url.URL)(nil) + _ = (*mail.Address)(nil) + _ = anypb.Any{} + _ = sort.Sort +) + +// Validate checks the field values on EntitlementImmutable with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *EntitlementImmutable) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on EntitlementImmutable with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// EntitlementImmutableMultiError, or nil if none found. +func (m *EntitlementImmutable) ValidateAll() error { + return m.validate(true) +} + +func (m *EntitlementImmutable) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for SourceId + + if all { + switch v := interface{}(m.GetMetadata()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, EntitlementImmutableValidationError{ + field: "Metadata", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, EntitlementImmutableValidationError{ + field: "Metadata", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetMetadata()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return EntitlementImmutableValidationError{ + field: "Metadata", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if len(errors) > 0 { + return EntitlementImmutableMultiError(errors) + } + + return nil +} + +// EntitlementImmutableMultiError is an error wrapping multiple validation +// errors returned by EntitlementImmutable.ValidateAll() if the designated +// constraints aren't met. +type EntitlementImmutableMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m EntitlementImmutableMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m EntitlementImmutableMultiError) AllErrors() []error { return m } + +// EntitlementImmutableValidationError is the validation error returned by +// EntitlementImmutable.Validate if the designated constraints aren't met. +type EntitlementImmutableValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e EntitlementImmutableValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e EntitlementImmutableValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e EntitlementImmutableValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e EntitlementImmutableValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e EntitlementImmutableValidationError) ErrorName() string { + return "EntitlementImmutableValidationError" +} + +// Error satisfies the builtin error interface +func (e EntitlementImmutableValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sEntitlementImmutable.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = EntitlementImmutableValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = EntitlementImmutableValidationError{} diff --git a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_external_ticket.pb.go b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_external_ticket.pb.go new file mode 100644 index 00000000..445884ae --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_external_ticket.pb.go @@ -0,0 +1,148 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: c1/connector/v2/annotation_external_ticket.proto + +package v2 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ExternalTicketSettings struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` +} + +func (x *ExternalTicketSettings) Reset() { + *x = ExternalTicketSettings{} + if protoimpl.UnsafeEnabled { + mi := &file_c1_connector_v2_annotation_external_ticket_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExternalTicketSettings) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExternalTicketSettings) ProtoMessage() {} + +func (x *ExternalTicketSettings) ProtoReflect() protoreflect.Message { + mi := &file_c1_connector_v2_annotation_external_ticket_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExternalTicketSettings.ProtoReflect.Descriptor instead. +func (*ExternalTicketSettings) Descriptor() ([]byte, []int) { + return file_c1_connector_v2_annotation_external_ticket_proto_rawDescGZIP(), []int{0} +} + +func (x *ExternalTicketSettings) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +var File_c1_connector_v2_annotation_external_ticket_proto protoreflect.FileDescriptor + +var file_c1_connector_v2_annotation_external_ticket_proto_rawDesc = []byte{ + 0x0a, 0x30, 0x63, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2f, 0x76, + 0x32, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x78, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x0f, 0x63, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x2e, 0x76, 0x32, 0x22, 0x32, 0x0a, 0x16, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x54, + 0x69, 0x63, 0x6b, 0x65, 0x74, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x18, 0x0a, + 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, + 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x64, 0x75, 0x63, 0x74, 0x6f, 0x72, 0x6f, + 0x6e, 0x65, 0x2f, 0x62, 0x61, 0x74, 0x6f, 0x6e, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x2f, + 0x63, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2f, 0x76, 0x32, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_c1_connector_v2_annotation_external_ticket_proto_rawDescOnce sync.Once + file_c1_connector_v2_annotation_external_ticket_proto_rawDescData = file_c1_connector_v2_annotation_external_ticket_proto_rawDesc +) + +func file_c1_connector_v2_annotation_external_ticket_proto_rawDescGZIP() []byte { + file_c1_connector_v2_annotation_external_ticket_proto_rawDescOnce.Do(func() { + file_c1_connector_v2_annotation_external_ticket_proto_rawDescData = protoimpl.X.CompressGZIP(file_c1_connector_v2_annotation_external_ticket_proto_rawDescData) + }) + return file_c1_connector_v2_annotation_external_ticket_proto_rawDescData +} + +var file_c1_connector_v2_annotation_external_ticket_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_c1_connector_v2_annotation_external_ticket_proto_goTypes = []interface{}{ + (*ExternalTicketSettings)(nil), // 0: c1.connector.v2.ExternalTicketSettings +} +var file_c1_connector_v2_annotation_external_ticket_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_c1_connector_v2_annotation_external_ticket_proto_init() } +func file_c1_connector_v2_annotation_external_ticket_proto_init() { + if File_c1_connector_v2_annotation_external_ticket_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_c1_connector_v2_annotation_external_ticket_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ExternalTicketSettings); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_c1_connector_v2_annotation_external_ticket_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_c1_connector_v2_annotation_external_ticket_proto_goTypes, + DependencyIndexes: file_c1_connector_v2_annotation_external_ticket_proto_depIdxs, + MessageInfos: file_c1_connector_v2_annotation_external_ticket_proto_msgTypes, + }.Build() + File_c1_connector_v2_annotation_external_ticket_proto = out.File + file_c1_connector_v2_annotation_external_ticket_proto_rawDesc = nil + file_c1_connector_v2_annotation_external_ticket_proto_goTypes = nil + file_c1_connector_v2_annotation_external_ticket_proto_depIdxs = nil +} diff --git a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_external_ticket.pb.validate.go b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_external_ticket.pb.validate.go new file mode 100644 index 00000000..abe6a364 --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_external_ticket.pb.validate.go @@ -0,0 +1,140 @@ +// Code generated by protoc-gen-validate. DO NOT EDIT. +// source: c1/connector/v2/annotation_external_ticket.proto + +package v2 + +import ( + "bytes" + "errors" + "fmt" + "net" + "net/mail" + "net/url" + "regexp" + "sort" + "strings" + "time" + "unicode/utf8" + + "google.golang.org/protobuf/types/known/anypb" +) + +// ensure the imports are used +var ( + _ = bytes.MinRead + _ = errors.New("") + _ = fmt.Print + _ = utf8.UTFMax + _ = (*regexp.Regexp)(nil) + _ = (*strings.Reader)(nil) + _ = net.IPv4len + _ = time.Duration(0) + _ = (*url.URL)(nil) + _ = (*mail.Address)(nil) + _ = anypb.Any{} + _ = sort.Sort +) + +// Validate checks the field values on ExternalTicketSettings with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *ExternalTicketSettings) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on ExternalTicketSettings with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// ExternalTicketSettingsMultiError, or nil if none found. +func (m *ExternalTicketSettings) ValidateAll() error { + return m.validate(true) +} + +func (m *ExternalTicketSettings) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Enabled + + if len(errors) > 0 { + return ExternalTicketSettingsMultiError(errors) + } + + return nil +} + +// ExternalTicketSettingsMultiError is an error wrapping multiple validation +// errors returned by ExternalTicketSettings.ValidateAll() if the designated +// constraints aren't met. +type ExternalTicketSettingsMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m ExternalTicketSettingsMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m ExternalTicketSettingsMultiError) AllErrors() []error { return m } + +// ExternalTicketSettingsValidationError is the validation error returned by +// ExternalTicketSettings.Validate if the designated constraints aren't met. +type ExternalTicketSettingsValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ExternalTicketSettingsValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ExternalTicketSettingsValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ExternalTicketSettingsValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ExternalTicketSettingsValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ExternalTicketSettingsValidationError) ErrorName() string { + return "ExternalTicketSettingsValidationError" +} + +// Error satisfies the builtin error interface +func (e ExternalTicketSettingsValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sExternalTicketSettings.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ExternalTicketSettingsValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ExternalTicketSettingsValidationError{} diff --git a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_grant.pb.go b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_grant.pb.go index 79578697..725f84a4 100644 --- a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_grant.pb.go +++ b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_grant.pb.go @@ -131,6 +131,61 @@ func (x *GrantExpandable) GetResourceTypeIds() []string { return nil } +type GrantImmutable struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SourceId string `protobuf:"bytes,1,opt,name=source_id,json=sourceId,proto3" json:"source_id,omitempty"` + Metadata *structpb.Struct `protobuf:"bytes,2,opt,name=metadata,proto3" json:"metadata,omitempty"` +} + +func (x *GrantImmutable) Reset() { + *x = GrantImmutable{} + if protoimpl.UnsafeEnabled { + mi := &file_c1_connector_v2_annotation_grant_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GrantImmutable) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GrantImmutable) ProtoMessage() {} + +func (x *GrantImmutable) ProtoReflect() protoreflect.Message { + mi := &file_c1_connector_v2_annotation_grant_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GrantImmutable.ProtoReflect.Descriptor instead. +func (*GrantImmutable) Descriptor() ([]byte, []int) { + return file_c1_connector_v2_annotation_grant_proto_rawDescGZIP(), []int{2} +} + +func (x *GrantImmutable) GetSourceId() string { + if x != nil { + return x.SourceId + } + return "" +} + +func (x *GrantImmutable) GetMetadata() *structpb.Struct { + if x != nil { + return x.Metadata + } + return nil +} + var File_c1_connector_v2_annotation_grant_proto protoreflect.FileDescriptor var file_c1_connector_v2_annotation_grant_proto_rawDesc = []byte{ @@ -152,10 +207,17 @@ var file_c1_connector_v2_annotation_grant_proto_rawDesc = []byte{ 0x6c, 0x6c, 0x6f, 0x77, 0x12, 0x2a, 0x0a, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x49, 0x64, 0x73, - 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, - 0x6f, 0x6e, 0x64, 0x75, 0x63, 0x74, 0x6f, 0x72, 0x6f, 0x6e, 0x65, 0x2f, 0x62, 0x61, 0x74, 0x6f, - 0x6e, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2f, 0x76, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x22, 0x62, 0x0a, 0x0e, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x49, 0x6d, 0x6d, 0x75, 0x74, 0x61, 0x62, + 0x6c, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, + 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x64, 0x75, 0x63, 0x74, 0x6f, 0x72, 0x6f, 0x6e, 0x65, 0x2f, + 0x62, 0x61, 0x74, 0x6f, 0x6e, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x31, 0x2f, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2f, 0x76, 0x32, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -170,19 +232,21 @@ func file_c1_connector_v2_annotation_grant_proto_rawDescGZIP() []byte { return file_c1_connector_v2_annotation_grant_proto_rawDescData } -var file_c1_connector_v2_annotation_grant_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_c1_connector_v2_annotation_grant_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_c1_connector_v2_annotation_grant_proto_goTypes = []interface{}{ (*GrantMetadata)(nil), // 0: c1.connector.v2.GrantMetadata (*GrantExpandable)(nil), // 1: c1.connector.v2.GrantExpandable - (*structpb.Struct)(nil), // 2: google.protobuf.Struct + (*GrantImmutable)(nil), // 2: c1.connector.v2.GrantImmutable + (*structpb.Struct)(nil), // 3: google.protobuf.Struct } var file_c1_connector_v2_annotation_grant_proto_depIdxs = []int32{ - 2, // 0: c1.connector.v2.GrantMetadata.metadata:type_name -> google.protobuf.Struct - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 3, // 0: c1.connector.v2.GrantMetadata.metadata:type_name -> google.protobuf.Struct + 3, // 1: c1.connector.v2.GrantImmutable.metadata:type_name -> google.protobuf.Struct + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_c1_connector_v2_annotation_grant_proto_init() } @@ -215,6 +279,18 @@ func file_c1_connector_v2_annotation_grant_proto_init() { return nil } } + file_c1_connector_v2_annotation_grant_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GrantImmutable); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -222,7 +298,7 @@ func file_c1_connector_v2_annotation_grant_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_c1_connector_v2_annotation_grant_proto_rawDesc, NumEnums: 0, - NumMessages: 2, + NumMessages: 3, NumExtensions: 0, NumServices: 0, }, diff --git a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_grant.pb.validate.go b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_grant.pb.validate.go index 05d3ff2d..98bfe0ec 100644 --- a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_grant.pb.validate.go +++ b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_grant.pb.validate.go @@ -265,3 +265,134 @@ var _ interface { Cause() error ErrorName() string } = GrantExpandableValidationError{} + +// Validate checks the field values on GrantImmutable with the rules defined in +// the proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *GrantImmutable) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on GrantImmutable with the rules defined +// in the proto definition for this message. If any rules are violated, the +// result is a list of violation errors wrapped in GrantImmutableMultiError, +// or nil if none found. +func (m *GrantImmutable) ValidateAll() error { + return m.validate(true) +} + +func (m *GrantImmutable) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for SourceId + + if all { + switch v := interface{}(m.GetMetadata()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, GrantImmutableValidationError{ + field: "Metadata", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, GrantImmutableValidationError{ + field: "Metadata", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetMetadata()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return GrantImmutableValidationError{ + field: "Metadata", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if len(errors) > 0 { + return GrantImmutableMultiError(errors) + } + + return nil +} + +// GrantImmutableMultiError is an error wrapping multiple validation errors +// returned by GrantImmutable.ValidateAll() if the designated constraints +// aren't met. +type GrantImmutableMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m GrantImmutableMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m GrantImmutableMultiError) AllErrors() []error { return m } + +// GrantImmutableValidationError is the validation error returned by +// GrantImmutable.Validate if the designated constraints aren't met. +type GrantImmutableValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e GrantImmutableValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e GrantImmutableValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e GrantImmutableValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e GrantImmutableValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e GrantImmutableValidationError) ErrorName() string { return "GrantImmutableValidationError" } + +// Error satisfies the builtin error interface +func (e GrantImmutableValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sGrantImmutable.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = GrantImmutableValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = GrantImmutableValidationError{} diff --git a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/ticket.pb.go b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/ticket.pb.go index 8731ec79..0415504d 100644 --- a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/ticket.pb.go +++ b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/ticket.pb.go @@ -1063,6 +1063,7 @@ type Ticket struct { CreatedAt *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,13,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` CompletedAt *timestamppb.Timestamp `protobuf:"bytes,14,opt,name=completed_at,json=completedAt,proto3" json:"completed_at,omitempty"` + RequestedFor *Resource `protobuf:"bytes,15,opt,name=requested_for,json=requestedFor,proto3" json:"requested_for,omitempty"` } func (x *Ticket) Reset() { @@ -1188,6 +1189,13 @@ func (x *Ticket) GetCompletedAt() *timestamppb.Timestamp { return nil } +func (x *Ticket) GetRequestedFor() *Resource { + if x != nil { + return x.RequestedFor + } + return nil +} + type TicketType struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1254,6 +1262,7 @@ type TicketRequest struct { Type *TicketType `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"` Labels []string `protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty"` CustomFields map[string]*TicketCustomField `protobuf:"bytes,6,rep,name=custom_fields,json=customFields,proto3" json:"custom_fields,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + RequestedFor *Resource `protobuf:"bytes,7,opt,name=requested_for,json=requestedFor,proto3" json:"requested_for,omitempty"` } func (x *TicketRequest) Reset() { @@ -1330,6 +1339,13 @@ func (x *TicketRequest) GetCustomFields() map[string]*TicketCustomField { return nil } +func (x *TicketRequest) GetRequestedFor() *Resource { + if x != nil { + return x.RequestedFor + } + return nil +} + type TicketsServiceCreateTicketRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1753,7 +1769,7 @@ var file_c1_connector_v2_ticket_proto_rawDesc = []byte{ 0x12, 0x36, 0x0a, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x0b, 0x61, 0x6e, 0x6e, - 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xc9, 0x05, 0x0a, 0x06, 0x54, 0x69, 0x63, + 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x89, 0x06, 0x0a, 0x06, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, @@ -1791,7 +1807,11 @@ var file_c1_connector_v2_ticket_proto_rawDesc = []byte{ 0x12, 0x3d, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x52, 0x0b, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x1a, + 0x6d, 0x70, 0x52, 0x0b, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, + 0x3e, 0x0a, 0x0d, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x66, 0x6f, 0x72, + 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x52, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x46, 0x6f, 0x72, 0x1a, 0x63, 0x0a, 0x11, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, @@ -1802,7 +1822,7 @@ var file_c1_connector_v2_ticket_proto_rawDesc = []byte{ 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, - 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x90, 0x03, 0x0a, 0x0d, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, + 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0xd0, 0x03, 0x0a, 0x0d, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, @@ -1821,7 +1841,11 @@ var file_c1_connector_v2_ticket_proto_rawDesc = []byte{ 0x6f, 0x72, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x46, 0x69, 0x65, - 0x6c, 0x64, 0x73, 0x1a, 0x63, 0x0a, 0x11, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x46, 0x69, 0x65, + 0x6c, 0x64, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, + 0x5f, 0x66, 0x6f, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x31, 0x2e, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, + 0x46, 0x6f, 0x72, 0x1a, 0x63, 0x0a, 0x11, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x31, 0x2e, 0x63, @@ -1979,33 +2003,35 @@ var file_c1_connector_v2_ticket_proto_depIdxs = []int32{ 26, // 27: c1.connector.v2.Ticket.created_at:type_name -> google.protobuf.Timestamp 26, // 28: c1.connector.v2.Ticket.updated_at:type_name -> google.protobuf.Timestamp 26, // 29: c1.connector.v2.Ticket.completed_at:type_name -> google.protobuf.Timestamp - 11, // 30: c1.connector.v2.TicketRequest.status:type_name -> c1.connector.v2.TicketStatus - 17, // 31: c1.connector.v2.TicketRequest.type:type_name -> c1.connector.v2.TicketType - 25, // 32: c1.connector.v2.TicketRequest.custom_fields:type_name -> c1.connector.v2.TicketRequest.CustomFieldsEntry - 18, // 33: c1.connector.v2.TicketsServiceCreateTicketRequest.request:type_name -> c1.connector.v2.TicketRequest - 0, // 34: c1.connector.v2.TicketsServiceCreateTicketRequest.schema:type_name -> c1.connector.v2.TicketSchema - 27, // 35: c1.connector.v2.TicketsServiceCreateTicketRequest.annotations:type_name -> google.protobuf.Any - 16, // 36: c1.connector.v2.TicketsServiceCreateTicketResponse.ticket:type_name -> c1.connector.v2.Ticket - 27, // 37: c1.connector.v2.TicketsServiceCreateTicketResponse.annotations:type_name -> google.protobuf.Any - 27, // 38: c1.connector.v2.TicketsServiceGetTicketRequest.annotations:type_name -> google.protobuf.Any - 16, // 39: c1.connector.v2.TicketsServiceGetTicketResponse.ticket:type_name -> c1.connector.v2.Ticket - 27, // 40: c1.connector.v2.TicketsServiceGetTicketResponse.annotations:type_name -> google.protobuf.Any - 1, // 41: c1.connector.v2.TicketSchema.CustomFieldsEntry.value:type_name -> c1.connector.v2.TicketCustomField - 1, // 42: c1.connector.v2.Ticket.CustomFieldsEntry.value:type_name -> c1.connector.v2.TicketCustomField - 1, // 43: c1.connector.v2.TicketRequest.CustomFieldsEntry.value:type_name -> c1.connector.v2.TicketCustomField - 19, // 44: c1.connector.v2.TicketsService.CreateTicket:input_type -> c1.connector.v2.TicketsServiceCreateTicketRequest - 21, // 45: c1.connector.v2.TicketsService.GetTicket:input_type -> c1.connector.v2.TicketsServiceGetTicketRequest - 14, // 46: c1.connector.v2.TicketsService.ListTicketSchemas:input_type -> c1.connector.v2.TicketsServiceListTicketSchemasRequest - 12, // 47: c1.connector.v2.TicketsService.GetTicketSchema:input_type -> c1.connector.v2.TicketsServiceGetTicketSchemaRequest - 20, // 48: c1.connector.v2.TicketsService.CreateTicket:output_type -> c1.connector.v2.TicketsServiceCreateTicketResponse - 22, // 49: c1.connector.v2.TicketsService.GetTicket:output_type -> c1.connector.v2.TicketsServiceGetTicketResponse - 15, // 50: c1.connector.v2.TicketsService.ListTicketSchemas:output_type -> c1.connector.v2.TicketsServiceListTicketSchemasResponse - 13, // 51: c1.connector.v2.TicketsService.GetTicketSchema:output_type -> c1.connector.v2.TicketsServiceGetTicketSchemaResponse - 48, // [48:52] is the sub-list for method output_type - 44, // [44:48] is the sub-list for method input_type - 44, // [44:44] is the sub-list for extension type_name - 44, // [44:44] is the sub-list for extension extendee - 0, // [0:44] is the sub-list for field type_name + 28, // 30: c1.connector.v2.Ticket.requested_for:type_name -> c1.connector.v2.Resource + 11, // 31: c1.connector.v2.TicketRequest.status:type_name -> c1.connector.v2.TicketStatus + 17, // 32: c1.connector.v2.TicketRequest.type:type_name -> c1.connector.v2.TicketType + 25, // 33: c1.connector.v2.TicketRequest.custom_fields:type_name -> c1.connector.v2.TicketRequest.CustomFieldsEntry + 28, // 34: c1.connector.v2.TicketRequest.requested_for:type_name -> c1.connector.v2.Resource + 18, // 35: c1.connector.v2.TicketsServiceCreateTicketRequest.request:type_name -> c1.connector.v2.TicketRequest + 0, // 36: c1.connector.v2.TicketsServiceCreateTicketRequest.schema:type_name -> c1.connector.v2.TicketSchema + 27, // 37: c1.connector.v2.TicketsServiceCreateTicketRequest.annotations:type_name -> google.protobuf.Any + 16, // 38: c1.connector.v2.TicketsServiceCreateTicketResponse.ticket:type_name -> c1.connector.v2.Ticket + 27, // 39: c1.connector.v2.TicketsServiceCreateTicketResponse.annotations:type_name -> google.protobuf.Any + 27, // 40: c1.connector.v2.TicketsServiceGetTicketRequest.annotations:type_name -> google.protobuf.Any + 16, // 41: c1.connector.v2.TicketsServiceGetTicketResponse.ticket:type_name -> c1.connector.v2.Ticket + 27, // 42: c1.connector.v2.TicketsServiceGetTicketResponse.annotations:type_name -> google.protobuf.Any + 1, // 43: c1.connector.v2.TicketSchema.CustomFieldsEntry.value:type_name -> c1.connector.v2.TicketCustomField + 1, // 44: c1.connector.v2.Ticket.CustomFieldsEntry.value:type_name -> c1.connector.v2.TicketCustomField + 1, // 45: c1.connector.v2.TicketRequest.CustomFieldsEntry.value:type_name -> c1.connector.v2.TicketCustomField + 19, // 46: c1.connector.v2.TicketsService.CreateTicket:input_type -> c1.connector.v2.TicketsServiceCreateTicketRequest + 21, // 47: c1.connector.v2.TicketsService.GetTicket:input_type -> c1.connector.v2.TicketsServiceGetTicketRequest + 14, // 48: c1.connector.v2.TicketsService.ListTicketSchemas:input_type -> c1.connector.v2.TicketsServiceListTicketSchemasRequest + 12, // 49: c1.connector.v2.TicketsService.GetTicketSchema:input_type -> c1.connector.v2.TicketsServiceGetTicketSchemaRequest + 20, // 50: c1.connector.v2.TicketsService.CreateTicket:output_type -> c1.connector.v2.TicketsServiceCreateTicketResponse + 22, // 51: c1.connector.v2.TicketsService.GetTicket:output_type -> c1.connector.v2.TicketsServiceGetTicketResponse + 15, // 52: c1.connector.v2.TicketsService.ListTicketSchemas:output_type -> c1.connector.v2.TicketsServiceListTicketSchemasResponse + 13, // 53: c1.connector.v2.TicketsService.GetTicketSchema:output_type -> c1.connector.v2.TicketsServiceGetTicketSchemaResponse + 50, // [50:54] is the sub-list for method output_type + 46, // [46:50] is the sub-list for method input_type + 46, // [46:46] is the sub-list for extension type_name + 46, // [46:46] is the sub-list for extension extendee + 0, // [0:46] is the sub-list for field type_name } func init() { file_c1_connector_v2_ticket_proto_init() } diff --git a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/ticket.pb.validate.go b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/ticket.pb.validate.go index 3d9a46d3..4aeef013 100644 --- a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/ticket.pb.validate.go +++ b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/ticket.pb.validate.go @@ -2853,6 +2853,35 @@ func (m *Ticket) validate(all bool) error { } } + if all { + switch v := interface{}(m.GetRequestedFor()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TicketValidationError{ + field: "RequestedFor", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TicketValidationError{ + field: "RequestedFor", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetRequestedFor()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TicketValidationError{ + field: "RequestedFor", + reason: "embedded message failed validation", + cause: err, + } + } + } + if len(errors) > 0 { return TicketMultiError(errors) } @@ -3163,6 +3192,35 @@ func (m *TicketRequest) validate(all bool) error { } } + if all { + switch v := interface{}(m.GetRequestedFor()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TicketRequestValidationError{ + field: "RequestedFor", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TicketRequestValidationError{ + field: "RequestedFor", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetRequestedFor()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TicketRequestValidationError{ + field: "RequestedFor", + reason: "embedded message failed validation", + cause: err, + } + } + } + if len(errors) > 0 { return TicketRequestMultiError(errors) } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/cli/cli.go b/vendor/github.com/conductorone/baton-sdk/pkg/cli/cli.go deleted file mode 100644 index 2b1918d8..00000000 --- a/vendor/github.com/conductorone/baton-sdk/pkg/cli/cli.go +++ /dev/null @@ -1,495 +0,0 @@ -package cli - -import ( - "bufio" - "context" - "encoding/base64" - "fmt" - "os" - - "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" - "github.com/spf13/cobra" - "go.uber.org/zap" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/anypb" - - "github.com/conductorone/baton-sdk/internal/connector" - v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" - v1 "github.com/conductorone/baton-sdk/pb/c1/connector_wrapper/v1" - "github.com/conductorone/baton-sdk/pkg/connectorrunner" - "github.com/conductorone/baton-sdk/pkg/logging" - "github.com/conductorone/baton-sdk/pkg/types" -) - -const ( - envPrefix = "baton" - defaultLogLevel = "info" - defaultLogFormat = logging.LogFormatJSON -) - -// NewCmd returns a new cobra command that will populate the provided config object, validate it, and run the provided run function. -func NewCmd[T any, PtrT *T]( - ctx context.Context, - name string, - cfg PtrT, - validateF func(ctx context.Context, cfg PtrT) error, - getConnector func(ctx context.Context, cfg PtrT) (types.ConnectorServer, error), - opts ...connectorrunner.Option, -) (*cobra.Command, error) { - err := setupService(name) - if err != nil { - return nil, err - } - - cmd := &cobra.Command{ - Use: name, - Short: name, - SilenceErrors: true, - SilenceUsage: true, - RunE: func(cmd *cobra.Command, args []string) error { - v, err := loadConfig(cmd, cfg) - if err != nil { - return err - } - - runCtx, err := initLogger( - ctx, - name, - logging.WithLogFormat(v.GetString("log-format")), - logging.WithLogLevel(v.GetString("log-level")), - ) - if err != nil { - return err - } - - err = validateF(ctx, cfg) - if err != nil { - return err - } - - l := ctxzap.Extract(runCtx) - - if isService() { - runCtx, err = runService(runCtx, name) - if err != nil { - l.Error("error running service", zap.Error(err)) - return err - } - } - - c, err := getConnector(runCtx, cfg) - if err != nil { - return err - } - - daemonMode := v.GetString("client-id") != "" || isService() - if daemonMode { - if v.GetString("client-id") == "" { - return fmt.Errorf("client-id is required in service mode") - } - if v.GetString("client-secret") == "" { - return fmt.Errorf("client-secret is required in service mode") - } - opts = append(opts, connectorrunner.WithClientCredentials(v.GetString("client-id"), v.GetString("client-secret"))) - } else { - switch { - case v.GetString("grant-entitlement") != "": - opts = append(opts, - connectorrunner.WithProvisioningEnabled(), - connectorrunner.WithOnDemandGrant( - v.GetString("file"), - v.GetString("grant-entitlement"), - v.GetString("grant-principal"), - v.GetString("grant-principal-type"), - )) - case v.GetString("revoke-grant") != "": - opts = append(opts, - connectorrunner.WithProvisioningEnabled(), - connectorrunner.WithOnDemandRevoke( - v.GetString("file"), - v.GetString("revoke-grant"), - )) - case v.GetBool("event-feed"): - opts = append(opts, connectorrunner.WithOnDemandEventStream()) - case v.GetString("create-account-login") != "": - opts = append(opts, - connectorrunner.WithProvisioningEnabled(), - connectorrunner.WithOnDemandCreateAccount( - v.GetString("file"), - v.GetString("create-account-login"), - v.GetString("create-account-email"), - )) - case v.GetString("delete-resource") != "": - opts = append(opts, - connectorrunner.WithProvisioningEnabled(), - connectorrunner.WithOnDemandDeleteResource( - v.GetString("file"), - v.GetString("delete-resource"), - v.GetString("delete-resource-type"), - )) - case v.GetString("rotate-credentials") != "": - opts = append(opts, - connectorrunner.WithProvisioningEnabled(), - connectorrunner.WithOnDemandRotateCredentials( - v.GetString("file"), - v.GetString("rotate-credentials"), - v.GetString("rotate-credentials-type"), - )) - case v.GetBool("create-ticket"): - opts = append(opts, - connectorrunner.WithTicketingEnabled(), - connectorrunner.WithCreateTicket(v.GetString("ticket-template-path"))) - case v.GetBool("list-ticket-schemas"): - opts = append(opts, - connectorrunner.WithTicketingEnabled(), - connectorrunner.WithListTicketSchemas()) - case v.GetBool("get-ticket"): - opts = append(opts, - connectorrunner.WithTicketingEnabled(), - connectorrunner.WithGetTicket(v.GetString("ticket-id"))) - default: - opts = append(opts, connectorrunner.WithOnDemandSync(v.GetString("file"))) - } - } - - if v.GetString("c1z-temp-dir") != "" { - c1zTmpDir := v.GetString("c1z-temp-dir") - if _, err := os.Stat(c1zTmpDir); os.IsNotExist(err) { - return fmt.Errorf("the specified c1z temp dir does not exist: %s", c1zTmpDir) - } - opts = append(opts, connectorrunner.WithTempDir(v.GetString("c1z-temp-dir"))) - } - - r, err := connectorrunner.NewConnectorRunner(runCtx, c, opts...) - if err != nil { - l.Error("error creating connector runner", zap.Error(err)) - return err - } - defer r.Close(runCtx) - - err = r.Run(runCtx) - if err != nil { - l.Error("error running connector", zap.Error(err)) - return err - } - - return nil - }, - } - - grpcServerCmd := &cobra.Command{ - Use: "_connector-service", - Short: "Start the connector service", - Hidden: true, - RunE: func(cmd *cobra.Command, args []string) error { - v, err := loadConfig(cmd, cfg) - if err != nil { - return err - } - - runCtx, err := initLogger( - ctx, - name, - logging.WithLogFormat(v.GetString("log-format")), - logging.WithLogLevel(v.GetString("log-level")), - ) - if err != nil { - return err - } - - err = validateF(runCtx, cfg) - if err != nil { - return err - } - - c, err := getConnector(runCtx, cfg) - if err != nil { - return err - } - - var copts []connector.Option - - if v.GetBool("provisioning") { - copts = append(copts, connector.WithProvisioningEnabled()) - } - - if v.GetBool("ticketing") { - copts = append(copts, connector.WithTicketingEnabled()) - } - - switch { - case v.GetString("grant-entitlement") != "": - copts = append(copts, connector.WithProvisioningEnabled()) - case v.GetString("revoke-grant") != "": - copts = append(copts, connector.WithProvisioningEnabled()) - case v.GetString("create-account-login") != "" || v.GetString("create-account-email") != "": - copts = append(copts, connector.WithProvisioningEnabled()) - case v.GetString("delete-resource") != "" || v.GetString("delete-resource-type") != "": - copts = append(copts, connector.WithProvisioningEnabled()) - case v.GetString("rotate-credentials") != "" || v.GetString("rotate-credentials-type") != "": - copts = append(copts, connector.WithProvisioningEnabled()) - case v.GetBool("create-ticket"): - copts = append(copts, connector.WithTicketingEnabled()) - case v.GetBool("list-ticket-schemas"): - copts = append(copts, connector.WithTicketingEnabled()) - case v.GetBool("get-ticket"): - copts = append(copts, connector.WithTicketingEnabled()) - } - - cw, err := connector.NewWrapper(runCtx, c, copts...) - if err != nil { - return err - } - - var cfgStr string - scn := bufio.NewScanner(os.Stdin) - for scn.Scan() { - cfgStr = scn.Text() - break - } - cfgBytes, err := base64.StdEncoding.DecodeString(cfgStr) - if err != nil { - return err - } - - go func() { - in := make([]byte, 1) - _, err := os.Stdin.Read(in) - if err != nil { - os.Exit(0) - } - }() - - if len(cfgBytes) == 0 { - return fmt.Errorf("unexpected empty input") - } - - serverCfg := &v1.ServerConfig{} - err = proto.Unmarshal(cfgBytes, serverCfg) - if err != nil { - return err - } - - err = serverCfg.ValidateAll() - if err != nil { - return err - } - - return cw.Run(runCtx, serverCfg) - }, - } - - capabilitiesCmd := &cobra.Command{ - Use: "capabilities", - Short: "Get connector capabilities", - RunE: func(cmd *cobra.Command, args []string) error { - v, err := loadConfig(cmd, cfg) - if err != nil { - return err - } - - runCtx, err := initLogger( - ctx, - name, - logging.WithLogFormat(v.GetString("log-format")), - logging.WithLogLevel(v.GetString("log-level")), - ) - if err != nil { - return err - } - - c, err := getConnector(runCtx, cfg) - if err != nil { - return err - } - - md, err := c.GetMetadata(runCtx, &v2.ConnectorServiceGetMetadataRequest{}) - if err != nil { - return err - } - - if md.Metadata.Capabilities == nil { - return fmt.Errorf("connector does not support capabilities") - } - - protoMarshaller := protojson.MarshalOptions{ - Multiline: true, - Indent: " ", - } - - a := &anypb.Any{} - err = anypb.MarshalFrom(a, md.Metadata.Capabilities, proto.MarshalOptions{Deterministic: true}) - if err != nil { - return err - } - - outBytes, err := protoMarshaller.Marshal(a) - if err != nil { - return err - } - - _, err = fmt.Fprint(os.Stdout, string(outBytes)) - if err != nil { - return err - } - - return nil - }, - } - - cmd.AddCommand(grpcServerCmd) - cmd.AddCommand(capabilitiesCmd) - - // Flags for file management - cmd.PersistentFlags().String("c1z-temp-dir", "", "The directory to store temporary files in. It "+ - "must exist, and write access is required. Defaults to the OS temporary directory. ($BATON_C1Z_TEMP_DIR)") - if err := cmd.PersistentFlags().MarkHidden("c1z-temp-dir"); err != nil { - return nil, err - } - - // Flags for logging configuration - cmd.PersistentFlags().String("log-level", defaultLogLevel, "The log level: debug, info, warn, error ($BATON_LOG_LEVEL)") - cmd.PersistentFlags().String("log-format", defaultLogFormat, "The output format for logs: json, console ($BATON_LOG_FORMAT)") - - // Flags for direct syncing and provisioning - cmd.PersistentFlags().StringP("file", "f", "sync.c1z", "The path to the c1z file to sync with ($BATON_FILE)") - - // TODO (ggreer): simplify command line flags. make one action and reuse entitlement, resource, etc. - // baton-connector --provision-action=grant --entitlement=entitlement_id --resource=resource_id --resource-type=resource_type - // baton-connector --provision-action=revoke --grant=grant_id - // baton-connector --provision-action=delete --resource-id=resource_id --resource-type=resource_type - // baton-connector --provision-action=create-account --login=login --email=email - // baton-connector --provision-action=rotate-credentials --resource-id=resource_id --resource-type=resource_type - - cmd.PersistentFlags().String("grant-entitlement", "", "The id of the entitlement to grant to the supplied principal ($BATON_GRANT_ENTITLEMENT)") - cmd.PersistentFlags().String("grant-principal", "", "The id of the resource to grant the entitlement to ($BATON_GRANT_PRINCIPAL)") - cmd.PersistentFlags().String("grant-principal-type", "", "The resource type of the principal to grant the entitlement to ($BATON_GRANT_PRINCIPAL_TYPE)") - cmd.PersistentFlags().String("revoke-grant", "", "The grant to revoke ($BATON_REVOKE_GRANT)") - cmd.PersistentFlags().Bool("event-feed", false, "Read feed events to stdout ($BATON_EVENT_FEED)") - cmd.MarkFlagsRequiredTogether("grant-entitlement", "grant-principal", "grant-principal-type") - - cmd.PersistentFlags().String("create-account-login", "", "The login of the account to create ($BATON_CREATE_ACCOUNT_LOGIN)") - cmd.PersistentFlags().String("create-account-email", "", "The email of the account to create ($BATON_CREATE_ACCOUNT_EMAIL)") - - cmd.PersistentFlags().String("delete-resource", "", "The id of the resource to delete ($BATON_DELETE_RESOURCE)") - cmd.PersistentFlags().String("delete-resource-type", "", "The type of the resource to delete ($BATON_DELETE_RESOURCE_TYPE)") - - cmd.PersistentFlags().String("rotate-credentials", "", "The id of the resource to rotate credentials on ($BATON_ROTATE_CREDENTIALS)") - cmd.PersistentFlags().String("rotate-credentials-type", "", "The type of the resource to rotate credentials on ($BATON_ROTATE_CREDENTIALS_TYPE)") - - cmd.PersistentFlags().Bool("ticketing", false, "This must be set to enable ticketing support ($BATON_TICKETING)") - cmd.PersistentFlags().Bool("create-ticket", false, "Create ticket ($BATON_CREATE_TICKET)") - cmd.PersistentFlags().String("ticket-template-path", "", "A JSON file describing the ticket to create ($BATON_TICKET_TEMPLATE_PATH)") - - cmd.PersistentFlags().Bool("list-ticket-schemas", false, "List ticket schemas ($BATON_LIST_SCHEMAS)") - - cmd.PersistentFlags().Bool("get-ticket", false, "Get ticket ($BATON_GET_TICKET)") - cmd.PersistentFlags().String("ticket-id", "", "The ID of the ticket to get ($BATON_TICKET_ID)") - - cmd.MarkFlagsMutuallyExclusive( - "grant-entitlement", - "revoke-grant", - "create-account-login", - "delete-resource", - "rotate-credentials", - "event-feed", - "create-ticket", - "get-ticket", - "list-ticket-schemas", - ) - cmd.MarkFlagsMutuallyExclusive( - "grant-entitlement", - "revoke-grant", - "create-account-email", - "delete-resource-type", - "rotate-credentials-type", - "event-feed", - "create-ticket", - "get-ticket", - "list-ticket-schemas", - ) - err = cmd.PersistentFlags().MarkHidden("grant-entitlement") - if err != nil { - return nil, err - } - err = cmd.PersistentFlags().MarkHidden("grant-principal") - if err != nil { - return nil, err - } - err = cmd.PersistentFlags().MarkHidden("grant-principal-type") - if err != nil { - return nil, err - } - err = cmd.PersistentFlags().MarkHidden("revoke-grant") - if err != nil { - return nil, err - } - err = cmd.PersistentFlags().MarkHidden("event-feed") - if err != nil { - return nil, err - } - err = cmd.PersistentFlags().MarkHidden("create-ticket") - if err != nil { - return nil, err - } - err = cmd.PersistentFlags().MarkHidden("ticket-template-path") - if err != nil { - return nil, err - } - err = cmd.PersistentFlags().MarkHidden("list-ticket-schemas") - if err != nil { - return nil, err - } - err = cmd.PersistentFlags().MarkHidden("get-ticket") - if err != nil { - return nil, err - } - err = cmd.PersistentFlags().MarkHidden("ticket-id") - if err != nil { - return nil, err - } - err = cmd.PersistentFlags().MarkHidden("create-account-login") - if err != nil { - return nil, err - } - err = cmd.PersistentFlags().MarkHidden("create-account-email") - if err != nil { - return nil, err - } - err = cmd.PersistentFlags().MarkHidden("delete-resource") - if err != nil { - return nil, err - } - err = cmd.PersistentFlags().MarkHidden("delete-resource-type") - if err != nil { - return nil, err - } - err = cmd.PersistentFlags().MarkHidden("rotate-credentials") - if err != nil { - return nil, err - } - err = cmd.PersistentFlags().MarkHidden("rotate-credentials-type") - if err != nil { - return nil, err - } - - // Flags for daemon mode - cmd.PersistentFlags().String("client-id", "", "The client ID used to authenticate with ConductorOne ($BATON_CLIENT_ID)") - cmd.PersistentFlags().String("client-secret", "", "The client secret used to authenticate with ConductorOne ($BATON_CLIENT_SECRET)") - cmd.PersistentFlags().BoolP("provisioning", "p", false, "This must be set in order for provisioning actions to be enabled. ($BATON_PROVISIONING)") - cmd.MarkFlagsRequiredTogether("client-id", "client-secret") - cmd.MarkFlagsMutuallyExclusive("file", "client-id") - cmd.MarkFlagsRequiredTogether("create-ticket", "ticket-template-path") - cmd.MarkFlagsRequiredTogether("get-ticket", "ticket-id") - // Add a hook for additional commands to be added to the root command. - // We use this for OS specific commands. - cmd.AddCommand(additionalCommands(name, cfg)...) - - err = configToCmdFlags(cmd, cfg) - if err != nil { - return nil, err - } - - return cmd, nil -} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/cli/commands.go b/vendor/github.com/conductorone/baton-sdk/pkg/cli/commands.go new file mode 100644 index 00000000..3f14d2ca --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/cli/commands.go @@ -0,0 +1,330 @@ +package cli + +import ( + "bufio" + "context" + "encoding/base64" + "fmt" + "os" + + "github.com/conductorone/baton-sdk/internal/connector" + v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" + v1 "github.com/conductorone/baton-sdk/pb/c1/connector_wrapper/v1" + "github.com/conductorone/baton-sdk/pkg/connectorrunner" + "github.com/conductorone/baton-sdk/pkg/field" + "github.com/conductorone/baton-sdk/pkg/logging" + "github.com/conductorone/baton-sdk/pkg/types" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" +) + +type GetConnectorFunc func(context.Context, *viper.Viper) (types.ConnectorServer, error) + +func MakeMainCommand( + ctx context.Context, + name string, + v *viper.Viper, + confschema field.Configuration, + getconnector GetConnectorFunc, + opts ...connectorrunner.Option, +) func(*cobra.Command, []string) error { + return func(*cobra.Command, []string) error { + // validate required fields and relationship constraints + if err := field.Validate(confschema, v); err != nil { + return err + } + + runCtx, err := initLogger( + ctx, + name, + logging.WithLogFormat(v.GetString("log-format")), + logging.WithLogLevel(v.GetString("log-level")), + ) + if err != nil { + return err + } + + l := ctxzap.Extract(runCtx) + + if isService() { + runCtx, err = runService(runCtx, name) + if err != nil { + l.Error("error running service", zap.Error(err)) + return err + } + } + + c, err := getconnector(runCtx, v) + if err != nil { + return err + } + + daemonMode := v.GetString("client-id") != "" || isService() + if daemonMode { + if v.GetString("client-id") == "" { + return fmt.Errorf("client-id is required in service mode") + } + if v.GetString("client-secret") == "" { + return fmt.Errorf("client-secret is required in service mode") + } + opts = append( + opts, + connectorrunner.WithClientCredentials( + v.GetString("client-id"), + v.GetString("client-secret"), + ), + ) + if v.GetBool("skip-full-sync") { + opts = append(opts, connectorrunner.WithFullSyncDisabled()) + } + } else { + switch { + case v.GetString("grant-entitlement") != "": + opts = append(opts, + connectorrunner.WithProvisioningEnabled(), + connectorrunner.WithOnDemandGrant( + v.GetString("file"), + v.GetString("grant-entitlement"), + v.GetString("grant-principal"), + v.GetString("grant-principal-type"), + )) + case v.GetString("revoke-grant") != "": + opts = append(opts, + connectorrunner.WithProvisioningEnabled(), + connectorrunner.WithOnDemandRevoke( + v.GetString("file"), + v.GetString("revoke-grant"), + )) + case v.GetBool("event-feed"): + opts = append(opts, connectorrunner.WithOnDemandEventStream()) + case v.GetString("create-account-login") != "": + opts = append(opts, + connectorrunner.WithProvisioningEnabled(), + connectorrunner.WithOnDemandCreateAccount( + v.GetString("file"), + v.GetString("create-account-login"), + v.GetString("create-account-email"), + )) + case v.GetString("delete-resource") != "": + opts = append(opts, + connectorrunner.WithProvisioningEnabled(), + connectorrunner.WithOnDemandDeleteResource( + v.GetString("file"), + v.GetString("delete-resource"), + v.GetString("delete-resource-type"), + )) + case v.GetString("rotate-credentials") != "": + opts = append(opts, + connectorrunner.WithProvisioningEnabled(), + connectorrunner.WithOnDemandRotateCredentials( + v.GetString("file"), + v.GetString("rotate-credentials"), + v.GetString("rotate-credentials-type"), + )) + case v.GetBool("create-ticket"): + opts = append(opts, + connectorrunner.WithTicketingEnabled(), + connectorrunner.WithCreateTicket(v.GetString("ticket-template-path"))) + case v.GetBool("list-ticket-schemas"): + opts = append(opts, + connectorrunner.WithTicketingEnabled(), + connectorrunner.WithListTicketSchemas()) + case v.GetBool("get-ticket"): + opts = append(opts, + connectorrunner.WithTicketingEnabled(), + connectorrunner.WithGetTicket(v.GetString("ticket-id"))) + default: + opts = append(opts, connectorrunner.WithOnDemandSync(v.GetString("file"))) + } + } + + if v.GetString("c1z-temp-dir") != "" { + c1zTmpDir := v.GetString("c1z-temp-dir") + if _, err := os.Stat(c1zTmpDir); os.IsNotExist(err) { + return fmt.Errorf("the specified c1z temp dir does not exist: %s", c1zTmpDir) + } + opts = append(opts, connectorrunner.WithTempDir(v.GetString("c1z-temp-dir"))) + } + + r, err := connectorrunner.NewConnectorRunner(runCtx, c, opts...) + if err != nil { + l.Error("error creating connector runner", zap.Error(err)) + return err + } + defer r.Close(runCtx) + + err = r.Run(runCtx) + if err != nil { + l.Error("error running connector", zap.Error(err)) + return err + } + + return nil + } +} + +func MakeGRPCServerCommand( + ctx context.Context, + name string, + v *viper.Viper, + confschema field.Configuration, + getconnector GetConnectorFunc, +) func(*cobra.Command, []string) error { + return func(*cobra.Command, []string) error { + // validate required fields and relationship constraints + if err := field.Validate(confschema, v); err != nil { + return err + } + + runCtx, err := initLogger( + ctx, + name, + logging.WithLogFormat(v.GetString("log-format")), + logging.WithLogLevel(v.GetString("log-level")), + ) + if err != nil { + return err + } + + c, err := getconnector(runCtx, v) + if err != nil { + return err + } + + var copts []connector.Option + + if v.GetBool("provisioning") { + copts = append(copts, connector.WithProvisioningEnabled()) + } + + if v.GetBool("ticketing") { + copts = append(copts, connector.WithTicketingEnabled()) + } + + if v.GetBool("skip-full-sync") { + copts = append(copts, connector.WithFullSyncDisabled()) + } + + switch { + case v.GetString("grant-entitlement") != "": + copts = append(copts, connector.WithProvisioningEnabled()) + case v.GetString("revoke-grant") != "": + copts = append(copts, connector.WithProvisioningEnabled()) + case v.GetString("create-account-login") != "" || v.GetString("create-account-email") != "": + copts = append(copts, connector.WithProvisioningEnabled()) + case v.GetString("delete-resource") != "" || v.GetString("delete-resource-type") != "": + copts = append(copts, connector.WithProvisioningEnabled()) + case v.GetString("rotate-credentials") != "" || v.GetString("rotate-credentials-type") != "": + copts = append(copts, connector.WithProvisioningEnabled()) + case v.GetBool("create-ticket"): + copts = append(copts, connector.WithTicketingEnabled()) + case v.GetBool("list-ticket-schemas"): + copts = append(copts, connector.WithTicketingEnabled()) + case v.GetBool("get-ticket"): + copts = append(copts, connector.WithTicketingEnabled()) + } + + cw, err := connector.NewWrapper(runCtx, c, copts...) + if err != nil { + return err + } + + var cfgStr string + scn := bufio.NewScanner(os.Stdin) + for scn.Scan() { + cfgStr = scn.Text() + break + } + cfgBytes, err := base64.StdEncoding.DecodeString(cfgStr) + if err != nil { + return err + } + + // NOTE (shackra): I don't understand this goroutine + go func() { + in := make([]byte, 1) + _, err := os.Stdin.Read(in) + if err != nil { + os.Exit(0) + } + }() + + if len(cfgBytes) == 0 { + return fmt.Errorf("unexpected empty input") + } + + serverCfg := &v1.ServerConfig{} + err = proto.Unmarshal(cfgBytes, serverCfg) + if err != nil { + return err + } + + err = serverCfg.ValidateAll() + if err != nil { + return err + } + + return cw.Run(runCtx, serverCfg) + } +} + +func MakeCapabilitiesCommand( + ctx context.Context, + name string, + v *viper.Viper, + getconnector GetConnectorFunc, +) func(*cobra.Command, []string) error { + return func(*cobra.Command, []string) error { + runCtx, err := initLogger( + ctx, + name, + logging.WithLogFormat(v.GetString("log-format")), + logging.WithLogLevel(v.GetString("log-level")), + ) + if err != nil { + return err + } + + c, err := getconnector(runCtx, v) + if err != nil { + return err + } + + md, err := c.GetMetadata(runCtx, &v2.ConnectorServiceGetMetadataRequest{}) + if err != nil { + return err + } + + if md.Metadata.Capabilities == nil { + return fmt.Errorf("connector does not support capabilities") + } + + protoMarshaller := protojson.MarshalOptions{ + Multiline: true, + Indent: " ", + } + + a := &anypb.Any{} + err = anypb.MarshalFrom(a, md.Metadata.Capabilities, proto.MarshalOptions{Deterministic: true}) + if err != nil { + return err + } + + outBytes, err := protoMarshaller.Marshal(a) + if err != nil { + return err + } + + _, err = fmt.Fprint(os.Stdout, string(outBytes)) + if err != nil { + return err + } + + return nil + } +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/cli/config.go b/vendor/github.com/conductorone/baton-sdk/pkg/cli/config.go deleted file mode 100644 index a271a3e6..00000000 --- a/vendor/github.com/conductorone/baton-sdk/pkg/cli/config.go +++ /dev/null @@ -1,146 +0,0 @@ -package cli - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "reflect" - "strconv" - "strings" - - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -type BaseConfig struct { - LogLevel string `mapstructure:"log-level"` - LogFormat string `mapstructure:"log-format"` - C1zPath string `mapstructure:"file"` - ClientID string `mapstructure:"client-id"` - ClientSecret string `mapstructure:"client-secret"` - GrantEntitlementID string `mapstructure:"grant-entitlement"` - GrantPrincipalID string `mapstructure:"grant-principal"` - GrantPrincipalType string `mapstructure:"grant-principal-type"` - RevokeGrantID string `mapstructure:"revoke-grant"` - C1zTempDir string `mapstructure:"c1z-temp-dir"` -} - -func getConfigPath(customPath string) (string, string, error) { - if customPath != "" { - cfgDir, cfgFile := filepath.Split(filepath.Clean(customPath)) - if cfgDir == "" { - cfgDir = "." - } - - ext := filepath.Ext(cfgFile) - if ext == "" || (ext != ".yaml" && ext != ".yml") { - return "", "", errors.New("expected config file to have .yaml or .yml extension") - } - - return strings.TrimSuffix(cfgDir, string(filepath.Separator)), strings.TrimSuffix(cfgFile, ext), nil - } - - return ".", ".baton", nil -} - -// loadConfig sets viper up to parse the config into the provided configuration object. -func loadConfig[T any, PtrT *T](cmd *cobra.Command, cfg PtrT) (*viper.Viper, error) { - v := viper.New() - v.SetConfigType("yaml") - - cfgPath, cfgName, err := getConfigPath(os.Getenv("BATON_CONFIG_PATH")) - if err != nil { - return nil, err - } - - v.SetConfigName(cfgName) - v.AddConfigPath(cfgPath) - - if err := v.ReadInConfig(); err != nil { - if ok := !errors.Is(err, viper.ConfigFileNotFoundError{}); !ok { - return nil, err - } - } - - v.SetEnvPrefix(envPrefix) - v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) - v.AutomaticEnv() - if err := v.BindPFlags(cmd.PersistentFlags()); err != nil { - return nil, err - } - if err := v.BindPFlags(cmd.Flags()); err != nil { - return nil, err - } - - if err := v.Unmarshal(cfg); err != nil { - return nil, err - } - - return v, nil -} - -func configToCmdFlags[T any, PtrT *T](cmd *cobra.Command, cfg PtrT) error { - baseConfigFields := reflect.VisibleFields(reflect.TypeOf(BaseConfig{})) - baseConfigFieldsMap := make(map[string]bool) - for _, field := range baseConfigFields { - baseConfigFieldsMap[field.Name] = true - } - - fields := reflect.VisibleFields(reflect.TypeOf(*cfg)) - for _, field := range fields { - // ignore BaseConfig fields - if _, ok := baseConfigFieldsMap[field.Name]; ok { - continue - } - if field.Name == "BaseConfig" { - continue - } - - cfgField := field.Tag.Get("mapstructure") - if cfgField == "" { - return fmt.Errorf("mapstructure tag is required on config field %s", field.Name) - } - description := field.Tag.Get("description") - if description == "" { - // Skip fields without descriptions for backwards compatibility - continue - } - defaultValueStr := field.Tag.Get("defaultValue") - - envVarName := strings.ReplaceAll(strings.ToUpper(cfgField), "-", "_") - description = fmt.Sprintf("%s ($BATON_%s)", description, envVarName) - switch field.Type.Kind() { - case reflect.String: - cmd.PersistentFlags().String(cfgField, defaultValueStr, description) - case reflect.Bool: - defaultValue, err := strconv.ParseBool(defaultValueStr) - if defaultValueStr != "" && err != nil { - return fmt.Errorf("invalid default value for config field %s: %w", field.Name, err) - } - cmd.PersistentFlags().Bool(cfgField, defaultValue, description) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - defaultValue, err := strconv.ParseInt(defaultValueStr, 10, 64) - if defaultValueStr != "" && err != nil { - return fmt.Errorf("invalid default value for config field %s: %w", field.Name, err) - } - cmd.PersistentFlags().Int64(cfgField, defaultValue, description) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - defaultValue, err := strconv.ParseUint(defaultValueStr, 10, 64) - if defaultValueStr != "" && err != nil { - return fmt.Errorf("invalid default value for config field %s: %w", field.Name, err) - } - cmd.PersistentFlags().Uint64(cfgField, defaultValue, description) - case reflect.Float32, reflect.Float64: - defaultValue, err := strconv.ParseFloat(defaultValueStr, 64) - if defaultValueStr != "" && err != nil { - return fmt.Errorf("invalid default value for config field %s: %w", field.Name, err) - } - cmd.PersistentFlags().Float64(cfgField, defaultValue, description) - default: - return fmt.Errorf("unsupported type %s for config field %s", field.Type.Kind(), field.Name) - } - } - - return nil -} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/cli/service_unix.go b/vendor/github.com/conductorone/baton-sdk/pkg/cli/service_unix.go index 19d95338..d9e8ad8f 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/cli/service_unix.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/cli/service_unix.go @@ -5,6 +5,7 @@ package cli import ( "context" + "github.com/conductorone/baton-sdk/pkg/field" "github.com/conductorone/baton-sdk/pkg/logging" "github.com/spf13/cobra" ) @@ -13,18 +14,14 @@ func isService() bool { return false } -func setupService(name string) error { - return nil -} - -func additionalCommands[T any, PtrT *T](connectorName string, cfg PtrT) []*cobra.Command { - return nil -} - -func runService(ctx context.Context, name string) (context.Context, error) { +func runService(ctx context.Context, _ string) (context.Context, error) { return ctx, nil } -func initLogger(ctx context.Context, name string, opts ...logging.Option) (context.Context, error) { +func initLogger(ctx context.Context, _ string, opts ...logging.Option) (context.Context, error) { return logging.Init(ctx, opts...) } + +func AdditionalCommands(_ string, _ []field.SchemaField) []*cobra.Command { + return nil +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/cli/service_windows.go b/vendor/github.com/conductorone/baton-sdk/pkg/cli/service_windows.go index ed2d4a93..5ba74988 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/cli/service_windows.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/cli/service_windows.go @@ -11,9 +11,9 @@ import ( "path/filepath" "reflect" "strconv" - "strings" "time" + "github.com/conductorone/baton-sdk/pkg/field" "github.com/conductorone/baton-sdk/pkg/logging" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "github.com/spf13/cobra" @@ -272,48 +272,50 @@ func getWindowsService(ctx context.Context, name string) (*mgr.Service, func(), }, nil } -func interactiveSetup[T any, PtrT *T](ctx context.Context, outputFilePath string, cfg PtrT) error { +func interactiveSetup(ctx context.Context, outputFilePath string, fields []field.SchemaField) error { l := ctxzap.Extract(ctx) - var ret []reflect.StructField - fields := reflect.VisibleFields(reflect.TypeOf(*cfg)) - for _, field := range fields { - if _, ok := skipServiceSetupFields[field.Name]; !ok { - ret = append(ret, field) + config := make(map[string]interface{}) + for _, vfield := range fields { + if vfield.GetName() == "" { + return fmt.Errorf("field has no name") } - } - config := make(map[string]interface{}) - for _, field := range ret { - var input string - cfgField := field.Tag.Get("mapstructure") - if cfgField == "" { - return fmt.Errorf("mapstructure tag is required on config field %s", field.Name) + // ignore any fields from the default set + if field.IsFieldAmongDefaultList(vfield) { + continue } - fmt.Printf("Enter %s: ", field.Name) + var input string + fmt.Printf("Enter %s: ", vfield.GetName()) scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { input = scanner.Text() break } - switch reflect.ValueOf(cfg).Elem().FieldByName(field.Name).Type() { - case stringReflectType: - config[cfgField] = input - - case boolReflectType: + switch vfield.GetType() { + case reflect.Bool: b, err := strconv.ParseBool(input) if err != nil { return err } - config[cfgField] = b + config[vfield.GetName()] = b - case stringSliceReflectType: - config[cfgField] = strings.Split(input, ",") + case reflect.String: + config[vfield.GetName()] = input + case reflect.Int: + i, err := strconv.Atoi(input) + if err != nil { + return err + } + + config[vfield.GetName()] = i + + // TODO (shackra): add support for []string in SDK default: - l.Error("Unsupported type for interactive config.", zap.String("type", field.Type.String())) + l.Error("Unsupported type for interactive config.", zap.String("type", vfield.GetType().String())) return errors.New("unsupported type for interactive config") } } @@ -349,52 +351,40 @@ func interactiveSetup[T any, PtrT *T](ctx context.Context, outputFilePath string return nil } -func installCmd[T any, PtrT *T](name string, cfg PtrT) *cobra.Command { +func installCmd(name string, fields []field.SchemaField) *cobra.Command { cmd := &cobra.Command{ Use: "setup", Short: fmt.Sprintf("Setup and configure the %s service", name), RunE: func(cmd *cobra.Command, args []string) error { - ctx, err := initLogger( - context.Background(), - name, - logging.WithLogFormat(logging.LogFormatConsole), - logging.WithLogLevel("info"), - ) - + ctx, err := initLogger(context.Background(), name, logging.WithLogFormat(logging.LogFormatConsole), logging.WithLogLevel("info")) l := ctxzap.Extract(ctx) - svcMgr, err := mgr.Connect() if err != nil { l.Error("Failed to connect to service manager.", zap.Error(err)) return err } defer svcMgr.Disconnect() - s, err := svcMgr.OpenService(name) if err == nil { s.Close() return fmt.Errorf("%s is already installed as a service. Please run '%s remove' to remove it first.", name, os.Args[0]) } - - err = interactiveSetup(ctx, filepath.Join(getConfigDir(name), defaultConfigFile), cfg) + err = interactiveSetup(ctx, filepath.Join(getConfigDir(name), defaultConfigFile), fields) if err != nil { l.Error("Failed to setup service.", zap.Error(err)) return err } - exePath, err := getExePath() if err != nil { l.Error("Failed to get executable path.", zap.Error(err)) return err } - s, err = svcMgr.CreateService(name, exePath, mgr.Config{DisplayName: name}) if err != nil { l.Error("Failed to create service.", zap.Error(err), zap.String("service_name", name)) return err } defer s.Close() - err = eventlog.InstallAsEventCreate(name, eventlog.Error|eventlog.Warning|eventlog.Info) if err != nil { l.Error("Failed to install event log source.", zap.Error(err)) @@ -405,7 +395,6 @@ func installCmd[T any, PtrT *T](name string, cfg PtrT) *cobra.Command { } return err } - l.Info("Successfully installed service.", zap.String("service_name", name)) return nil }, @@ -521,12 +510,12 @@ func runService(ctx context.Context, name string) (context.Context, error) { return ctx, nil } -func additionalCommands[T any, PtrT *T](connectorName string, cfg PtrT) []*cobra.Command { +func AdditionalCommands(connectorName string, fields []field.SchemaField) []*cobra.Command { return []*cobra.Command{ startCmd(connectorName), stopCmd(connectorName), statusCmd(connectorName), - installCmd(connectorName, cfg), + installCmd(connectorName, fields), uninstallCmd(connectorName), } } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/config/config.go b/vendor/github.com/conductorone/baton-sdk/pkg/config/config.go new file mode 100644 index 00000000..45d2b093 --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/config/config.go @@ -0,0 +1,228 @@ +package config + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + + "github.com/conductorone/baton-sdk/pkg/cli" + "github.com/conductorone/baton-sdk/pkg/connectorrunner" + "github.com/conductorone/baton-sdk/pkg/field" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +func DefineConfiguration( + ctx context.Context, + connectorName string, + connector cli.GetConnectorFunc, + schema field.Configuration, + options ...connectorrunner.Option, +) (*viper.Viper, *cobra.Command, error) { + v := viper.New() + v.SetConfigType("yaml") + + path, name, err := cleanOrGetConfigPath(os.Getenv("BATON_CONFIG_PATH")) + if err != nil { + return nil, nil, err + } + + v.SetConfigName(name) + v.AddConfigPath(path) + if err := v.ReadInConfig(); err != nil { + if errors.Is(err, viper.ConfigFileNotFoundError{}) { + return nil, nil, err + } + } + v.SetEnvPrefix("baton") + v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + v.AutomaticEnv() + + // add default fields and constrains + schema.Fields = field.EnsureDefaultFieldsExists(schema.Fields) + schema.Constraints = field.EnsureDefaultRelationships(schema.Constraints) + + // setup CLI with cobra + mainCMD := &cobra.Command{ + Use: connectorName, + Short: connectorName, + SilenceErrors: true, + SilenceUsage: true, + RunE: cli.MakeMainCommand(ctx, connectorName, v, schema, connector, options...), + } + + // add options to the main command + for _, field := range schema.Fields { + switch field.FieldType { + case reflect.Bool: + value, err := field.Bool() + if err != nil { + return nil, nil, fmt.Errorf( + "field %s, %s: %w", + field.FieldName, + field.FieldType, + err, + ) + } + mainCMD.PersistentFlags(). + BoolP(field.FieldName, field.CLIShortHand, value, field.GetDescription()) + case reflect.Int: + value, err := field.Int() + if err != nil { + return nil, nil, fmt.Errorf( + "field %s, %s: %w", + field.FieldName, + field.FieldType, + err, + ) + } + mainCMD.PersistentFlags(). + IntP(field.FieldName, field.CLIShortHand, value, field.GetDescription()) + case reflect.String: + value, err := field.String() + if err != nil { + return nil, nil, fmt.Errorf( + "field %s, %s: %w", + field.FieldName, + field.FieldType, + err, + ) + } + mainCMD.PersistentFlags(). + StringP(field.FieldName, field.CLIShortHand, value, field.GetDescription()) + case reflect.Slice: + value, err := field.StringSlice() + if err != nil { + return nil, nil, fmt.Errorf( + "field %s, %s: %w", + field.FieldName, + field.FieldType, + err, + ) + } + mainCMD.PersistentFlags(). + StringSliceP(field.FieldName, field.CLIShortHand, value, field.GetDescription()) + default: + return nil, nil, fmt.Errorf( + "field %s, %s is not yet supported", + field.FieldName, + field.FieldType, + ) + } + + // mark hidden + if field.Hidden { + err := mainCMD.PersistentFlags().MarkHidden(field.FieldName) + if err != nil { + return nil, nil, fmt.Errorf( + "cannot hide field %s, %s: %w", + field.FieldName, + field.FieldType, + err, + ) + } + } + + // mark required + if field.Required { + if field.FieldType == reflect.Bool { + return nil, nil, fmt.Errorf("requiring %s of type %s does not make sense", field.FieldName, field.FieldType) + } + + err := mainCMD.MarkPersistentFlagRequired(field.FieldName) + if err != nil { + return nil, nil, fmt.Errorf( + "cannot require field %s, %s: %w", + field.FieldName, + field.FieldType, + err, + ) + } + } + } + + // apply constrains + for _, constrain := range schema.Constraints { + switch constrain.Kind { + case field.MutuallyExclusive: + mainCMD.MarkFlagsMutuallyExclusive(listFieldConstrainsAsStrings(constrain)...) + case field.RequiredTogether: + mainCMD.MarkFlagsRequiredTogether(listFieldConstrainsAsStrings(constrain)...) + case field.AtLeastOne: + mainCMD.MarkFlagsOneRequired(listFieldConstrainsAsStrings(constrain)...) + case field.Dependents: + // do nothing + } + } + + if err := v.BindPFlags(mainCMD.PersistentFlags()); err != nil { + return nil, nil, err + } + if err := v.BindPFlags(mainCMD.Flags()); err != nil { + return nil, nil, err + } + + grpcServerCmd := &cobra.Command{ + Use: "_connector-service", + Short: "Start the connector service", + Hidden: true, + RunE: cli.MakeGRPCServerCommand(ctx, connectorName, v, schema, connector), + } + mainCMD.AddCommand(grpcServerCmd) + + capabilitiesCmd := &cobra.Command{ + Use: "capabilities", + Short: "Get connector capabilities", + RunE: cli.MakeCapabilitiesCommand(ctx, connectorName, v, connector), + } + mainCMD.AddCommand(capabilitiesCmd) + + mainCMD.AddCommand(cli.AdditionalCommands(name, schema.Fields)...) + + // NOTE (shackra): we don't check subcommands (i.e.: grpcServerCmd and capabilitiesCmd) + mainCMD.PersistentFlags().VisitAll(func(f *pflag.Flag) { + if v.IsSet(f.Name) { + _ = mainCMD.Flags().Set(f.Name, v.GetString(f.Name)) + } + }) + + return v, mainCMD, nil +} + +func listFieldConstrainsAsStrings(constrains field.SchemaFieldRelationship) []string { + var fields []string + for _, v := range constrains.Fields { + fields = append(fields, v.FieldName) + } + + return fields +} + +func cleanOrGetConfigPath(customPath string) (string, string, error) { + if customPath != "" { + cfgDir, cfgFile := filepath.Split(filepath.Clean(customPath)) + if cfgDir == "" { + cfgDir = "." + } + + ext := filepath.Ext(cfgFile) + if ext == "" || (ext != ".yaml" && ext != ".yml") { + return "", "", errors.New("expected config file to have .yaml or .yml extension") + } + + return strings.TrimSuffix( + cfgDir, + string(filepath.Separator), + ), strings.TrimSuffix( + cfgFile, + ext, + ), nil + } + + return ".", ".baton", nil +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/connectorbuilder/connectorbuilder.go b/vendor/github.com/conductorone/baton-sdk/pkg/connectorbuilder/connectorbuilder.go index 97b91cfb..48616d4a 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/connectorbuilder/connectorbuilder.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/connectorbuilder/connectorbuilder.go @@ -2,6 +2,7 @@ package connectorbuilder import ( "context" + "errors" "fmt" "sort" "time" @@ -86,6 +87,7 @@ type builderImpl struct { eventFeed EventProvider cb ConnectorBuilder ticketManager TicketManager + ticketingEnabled bool m *metrics.M nowFunc func() time.Time } @@ -139,6 +141,7 @@ func (b *builderImpl) CreateTicket(ctx context.Context, request *v2.TicketsServi Type: reqBody.GetType(), Labels: reqBody.GetLabels(), CustomFields: reqBody.GetCustomFields(), + RequestedFor: reqBody.GetRequestedFor(), } ticket, annos, err := b.ticketManager.CreateTicket(ctx, cTicket, request.GetSchema()) @@ -289,6 +292,16 @@ func NewConnector(ctx context.Context, in interface{}, opts ...Opt) (types.Conne type Opt func(b *builderImpl) error +func WithTicketingEnabled() Opt { + return func(b *builderImpl) error { + if _, ok := b.cb.(TicketManager); ok { + b.ticketingEnabled = true + return nil + } + return errors.New("external ticketing not supported") + } +} + func WithMetricsHandler(h metrics.Handler) Opt { return func(b *builderImpl) error { b.m = metrics.New(h) @@ -441,6 +454,12 @@ func (b *builderImpl) GetMetadata(ctx context.Context, request *v2.ConnectorServ md.Capabilities = getCapabilities(ctx, b) + annos := annotations.Annotations(md.Annotations) + if b.ticketManager != nil { + annos.Append(&v2.ExternalTicketSettings{Enabled: b.ticketingEnabled}) + } + md.Annotations = annos + b.m.RecordTaskSuccess(ctx, tt, b.nowFunc().Sub(start)) return &v2.ConnectorServiceGetMetadataResponse{Metadata: md}, nil } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/connectorrunner/runner.go b/vendor/github.com/conductorone/baton-sdk/pkg/connectorrunner/runner.go index 86a3f390..7dc9cb48 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/connectorrunner/runner.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/connectorrunner/runner.go @@ -249,6 +249,7 @@ type runnerConfig struct { createTicketConfig *createTicketConfig listTicketSchemasConfig *listTicketSchemasConfig getTicketConfig *getTicketConfig + skipFullSync bool } // WithRateLimiterConfig sets the RateLimiterConfig for a runner. @@ -416,6 +417,13 @@ func WithProvisioningEnabled() Option { } } +func WithFullSyncDisabled() Option { + return func(ctx context.Context, cfg *runnerConfig) error { + cfg.skipFullSync = true + return nil + } +} + func WithTicketingEnabled() Option { return func(ctx context.Context, cfg *runnerConfig) error { cfg.ticketingEnabled = true @@ -485,6 +493,10 @@ func NewConnectorRunner(ctx context.Context, c types.ConnectorServer, opts ...Op wrapperOpts = append(wrapperOpts, connector.WithTicketingEnabled()) } + if cfg.skipFullSync { + wrapperOpts = append(wrapperOpts, connector.WithFullSyncDisabled()) + } + cw, err := connector.NewWrapper(ctx, c, wrapperOpts...) if err != nil { return nil, err @@ -541,7 +553,7 @@ func NewConnectorRunner(ctx context.Context, c types.ConnectorServer, opts ...Op return runner, nil } - tm, err := c1api.NewC1TaskManager(ctx, cfg.clientID, cfg.clientSecret, cfg.tempDir) + tm, err := c1api.NewC1TaskManager(ctx, cfg.clientID, cfg.clientSecret, cfg.tempDir, cfg.skipFullSync) if err != nil { return nil, err } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/connectorstore/connectorstore.go b/vendor/github.com/conductorone/baton-sdk/pkg/connectorstore/connectorstore.go index 596278aa..88ac4e5c 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/connectorstore/connectorstore.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/connectorstore/connectorstore.go @@ -33,13 +33,15 @@ type Reader interface { type Writer interface { Reader StartSync(ctx context.Context) (string, bool, error) + StartNewSync(ctx context.Context) (string, error) CurrentSyncStep(ctx context.Context) (string, error) CheckpointSync(ctx context.Context, syncToken string) error EndSync(ctx context.Context) error - PutResourceType(ctx context.Context, resourceType *v2.ResourceType) error - PutResource(ctx context.Context, resource *v2.Resource) error - PutEntitlement(ctx context.Context, entitlement *v2.Entitlement) error - PutGrant(ctx context.Context, grant *v2.Grant) error PutAsset(ctx context.Context, assetRef *v2.AssetRef, contentType string, data []byte) error Cleanup(ctx context.Context) error + + PutGrants(ctx context.Context, grants ...*v2.Grant) error + PutResourceTypes(ctx context.Context, resourceTypes ...*v2.ResourceType) error + PutResources(ctx context.Context, resources ...*v2.Resource) error + PutEntitlements(ctx context.Context, entitlements ...*v2.Entitlement) error } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/assets.go b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/assets.go index d3f723f7..2347a5be 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/assets.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/assets.go @@ -9,7 +9,6 @@ import ( "github.com/doug-martin/goqu/v9" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" - "go.uber.org/zap" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" ) @@ -64,8 +63,6 @@ func (c *C1File) PutAsset(ctx context.Context, assetRef *v2.AssetRef, contentTyp contentType = "unknown" } - l.Debug("syncing asset", zap.String("content_type", contentType), zap.Int("asset_size", len(data))) - err := c.validateSyncDb(ctx) if err != nil { return err @@ -101,8 +98,6 @@ func (c *C1File) PutAsset(ctx context.Context, assetRef *v2.AssetRef, contentTyp // GetAsset fetches the specified asset from the database, and returns the content type and an io.Reader for the caller to // read the asset from. func (c *C1File) GetAsset(ctx context.Context, request *v2.AssetServiceGetAssetRequest) (string, io.Reader, error) { - ctxzap.Extract(ctx).Debug("fetching asset", zap.String("id", request.Asset.Id)) - err := c.validateDb(ctx) if err != nil { return "", nil, err diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/c1file.go b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/c1file.go index b33d1c34..04a35292 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/c1file.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/c1file.go @@ -8,9 +8,17 @@ import ( "path/filepath" "github.com/doug-martin/goqu/v9" + // NOTE: required to register the dialect for goqu. + // + // If you remove this import, goqu.Dialect("sqlite3") will + // return a copy of the default dialect, which is not what we want, + // and allocates a ton of memory. + _ "github.com/doug-martin/goqu/v9/dialect/sqlite3" + _ "github.com/glebarez/go-sqlite" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" + "github.com/conductorone/baton-sdk/pkg/connectorstore" ) type pragma struct { @@ -30,6 +38,8 @@ type C1File struct { pragmas []pragma } +var _ connectorstore.Writer = (*C1File)(nil) + type C1FOption func(*C1File) func WithC1FTmpDir(tempDir string) C1FOption { @@ -50,6 +60,7 @@ func NewC1File(ctx context.Context, dbFilePath string, opts ...C1FOption) (*C1Fi if err != nil { return nil, err } + db := goqu.New("sqlite3", rawDB) c1File := &C1File{ diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/entitlements.go b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/entitlements.go index f91d5536..7191d217 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/entitlements.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/entitlements.go @@ -5,8 +5,6 @@ import ( "fmt" "github.com/doug-martin/goqu/v9" - "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" - "go.uber.org/zap" "google.golang.org/protobuf/proto" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" @@ -51,7 +49,6 @@ func (r *entitlementsTable) Schema() (string, []interface{}) { } func (c *C1File) ListEntitlements(ctx context.Context, request *v2.EntitlementsServiceListEntitlementsRequest) (*v2.EntitlementsServiceListEntitlementsResponse, error) { - ctxzap.Extract(ctx).Debug("listing entitlements") objs, nextPageToken, err := c.listConnectorObjects(ctx, entitlements.Name(), request) if err != nil { return nil, fmt.Errorf("error listing entitlements: %w", err) @@ -74,8 +71,6 @@ func (c *C1File) ListEntitlements(ctx context.Context, request *v2.EntitlementsS } func (c *C1File) GetEntitlement(ctx context.Context, request *reader_v2.EntitlementsReaderServiceGetEntitlementRequest) (*reader_v2.EntitlementsReaderServiceGetEntitlementResponse, error) { - ctxzap.Extract(ctx).Debug("fetching entitlement", zap.String("entitlement_id", request.EntitlementId)) - ret := &v2.Entitlement{} err := c.getConnectorObject(ctx, entitlements.Name(), request.EntitlementId, ret) @@ -88,27 +83,26 @@ func (c *C1File) GetEntitlement(ctx context.Context, request *reader_v2.Entitlem }, nil } -func (c *C1File) PutEntitlement(ctx context.Context, entitlement *v2.Entitlement) error { - ctxzap.Extract(ctx).Debug("syncing entitlement", zap.String("entitlement_id", entitlement.Id)) - - if entitlement.Resource == nil && entitlement.Resource.Id == nil { - return fmt.Errorf("entitlements must have a non-nil resource") - } +func (c *C1File) PutEntitlements(ctx context.Context, entitlementObjs ...*v2.Entitlement) error { + err := c.db.WithTx(func(tx *goqu.TxDatabase) error { + err := bulkPutConnectorObjectTx(ctx, c, tx, entitlements.Name(), + func(entitlement *v2.Entitlement) (goqu.Record, error) { + return goqu.Record{ + "resource_id": entitlement.Resource.Id.Resource, + "resource_type_id": entitlement.Resource.Id.ResourceType, + }, nil + }, + entitlementObjs..., + ) + if err != nil { + return err + } - query, args, err := c.putConnectorObjectQuery(ctx, entitlements.Name(), entitlement, goqu.Record{ - "resource_id": entitlement.Resource.Id.Resource, - "resource_type_id": entitlement.Resource.Id.ResourceType, + return nil }) if err != nil { return err } - - _, err = c.db.ExecContext(ctx, query, args...) - if err != nil { - return err - } - c.dbUpdated = true - return nil } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/grants.go b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/grants.go index d515f263..0a3cf5a9 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/grants.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/grants.go @@ -5,8 +5,6 @@ import ( "fmt" "github.com/doug-martin/goqu/v9" - "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" - "go.uber.org/zap" "google.golang.org/protobuf/proto" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" @@ -60,8 +58,6 @@ func (r *grantsTable) Schema() (string, []interface{}) { } func (c *C1File) ListGrants(ctx context.Context, request *v2.GrantsServiceListGrantsRequest) (*v2.GrantsServiceListGrantsResponse, error) { - ctxzap.Extract(ctx).Debug("listing grants") - objs, nextPageToken, err := c.listConnectorObjects(ctx, grants.Name(), request) if err != nil { return nil, fmt.Errorf("error listing grants: %w", err) @@ -84,8 +80,6 @@ func (c *C1File) ListGrants(ctx context.Context, request *v2.GrantsServiceListGr } func (c *C1File) GetGrant(ctx context.Context, request *reader_v2.GrantsReaderServiceGetGrantRequest) (*reader_v2.GrantsReaderServiceGetGrantResponse, error) { - ctxzap.Extract(ctx).Debug("fetching grant", zap.String("grant_id", request.GrantId)) - ret := &v2.Grant{} err := c.getConnectorObject(ctx, grants.Name(), request.GrantId, ret) @@ -102,8 +96,6 @@ func (c *C1File) ListGrantsForEntitlement( ctx context.Context, request *reader_v2.GrantsReaderServiceListGrantsForEntitlementRequest, ) (*reader_v2.GrantsReaderServiceListGrantsForEntitlementResponse, error) { - ctxzap.Extract(ctx).Debug("listing grants for entitlement", zap.Any("entitlement", request.Entitlement)) - objs, nextPageToken, err := c.listConnectorObjects(ctx, grants.Name(), request) if err != nil { return nil, fmt.Errorf("error listing grants for entitlement '%s': %w", request.GetEntitlement().GetId(), err) @@ -129,8 +121,6 @@ func (c *C1File) ListGrantsForPrincipal( ctx context.Context, request *reader_v2.GrantsReaderServiceListGrantsForEntitlementRequest, ) (*reader_v2.GrantsReaderServiceListGrantsForEntitlementResponse, error) { - ctxzap.Extract(ctx).Debug("listing grants for principal", zap.Any("principal", request.GetPrincipalId())) - objs, nextPageToken, err := c.listConnectorObjects(ctx, grants.Name(), request) if err != nil { return nil, fmt.Errorf("error listing grants for principal '%s': %w", request.GetPrincipalId(), err) @@ -156,8 +146,6 @@ func (c *C1File) ListGrantsForResourceType( ctx context.Context, request *reader_v2.GrantsReaderServiceListGrantsForResourceTypeRequest, ) (*reader_v2.GrantsReaderServiceListGrantsForResourceTypeResponse, error) { - ctxzap.Extract(ctx).Debug("listing grants for resource type", zap.String("resource_type_id", request.GetResourceTypeId())) - objs, nextPageToken, err := c.listConnectorObjects(ctx, grants.Name(), request) if err != nil { return nil, fmt.Errorf("error listing grants for resource type '%s': %w", request.GetResourceTypeId(), err) @@ -179,26 +167,28 @@ func (c *C1File) ListGrantsForResourceType( }, nil } -func (c *C1File) PutGrant(ctx context.Context, grant *v2.Grant) error { - ctxzap.Extract(ctx).Debug("syncing grant", zap.String("grant_id", grant.Id)) - - query, args, err := c.putConnectorObjectQuery(ctx, grants.Name(), grant, goqu.Record{ - "resource_type_id": grant.Entitlement.Resource.Id.ResourceType, - "resource_id": grant.Entitlement.Resource.Id.Resource, - "entitlement_id": grant.Entitlement.Id, - "principal_resource_type_id": grant.Principal.Id.ResourceType, - "principal_resource_id": grant.Principal.Id.Resource, +func (c *C1File) PutGrants(ctx context.Context, bulkGrants ...*v2.Grant) error { + err := c.db.WithTx(func(tx *goqu.TxDatabase) error { + err := bulkPutConnectorObjectTx(ctx, c, tx, grants.Name(), + func(grant *v2.Grant) (goqu.Record, error) { + return goqu.Record{ + "resource_type_id": grant.Entitlement.Resource.Id.ResourceType, + "resource_id": grant.Entitlement.Resource.Id.Resource, + "entitlement_id": grant.Entitlement.Id, + "principal_resource_type_id": grant.Principal.Id.ResourceType, + "principal_resource_id": grant.Principal.Id.Resource, + }, nil + }, + bulkGrants..., + ) + if err != nil { + return err + } + return nil }) if err != nil { return err } - - _, err = c.db.ExecContext(ctx, query, args...) - if err != nil { - return err - } - c.dbUpdated = true - return nil } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/resouce_types.go b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/resouce_types.go index b3f8eca4..a0bb4d71 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/resouce_types.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/resouce_types.go @@ -4,12 +4,11 @@ import ( "context" "fmt" - "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" - "go.uber.org/zap" "google.golang.org/protobuf/proto" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" reader_v2 "github.com/conductorone/baton-sdk/pb/c1/reader/v2" + "github.com/doug-martin/goqu/v9" ) const resourceTypesTableVersion = "1" @@ -45,8 +44,6 @@ func (r *resourceTypesTable) Schema() (string, []interface{}) { } func (c *C1File) ListResourceTypes(ctx context.Context, request *v2.ResourceTypesServiceListResourceTypesRequest) (*v2.ResourceTypesServiceListResourceTypesResponse, error) { - ctxzap.Extract(ctx).Debug("listing resource types") - objs, nextPageToken, err := c.listConnectorObjects(ctx, resourceTypes.Name(), request) if err != nil { return nil, fmt.Errorf("error listing resource types: %w", err) @@ -69,8 +66,6 @@ func (c *C1File) ListResourceTypes(ctx context.Context, request *v2.ResourceType } func (c *C1File) GetResourceType(ctx context.Context, request *reader_v2.ResourceTypesReaderServiceGetResourceTypeRequest) (*reader_v2.ResourceTypesReaderServiceGetResourceTypeResponse, error) { - ctxzap.Extract(ctx).Debug("fetching resource type", zap.String("resource_type_id", request.ResourceTypeId)) - ret := &v2.ResourceType{} err := c.getConnectorObject(ctx, resourceTypes.Name(), request.ResourceTypeId, ret) @@ -83,20 +78,22 @@ func (c *C1File) GetResourceType(ctx context.Context, request *reader_v2.Resourc }, nil } -func (c *C1File) PutResourceType(ctx context.Context, resourceType *v2.ResourceType) error { - ctxzap.Extract(ctx).Debug("syncing resource type", zap.String("resource_type_id", resourceType.Id)) - - query, args, err := c.putConnectorObjectQuery(ctx, resourceTypes.Name(), resourceType, nil) - if err != nil { - return err - } - - _, err = c.db.ExecContext(ctx, query, args...) +func (c *C1File) PutResourceTypes(ctx context.Context, resourceTypesObjs ...*v2.ResourceType) error { + err := c.db.WithTx(func(tx *goqu.TxDatabase) error { + err := bulkPutConnectorObjectTx(ctx, c, tx, resourceTypes.Name(), + func(resource *v2.ResourceType) (goqu.Record, error) { + return nil, nil + }, + resourceTypesObjs..., + ) + if err != nil { + return err + } + return nil + }) if err != nil { return err } - c.dbUpdated = true - return nil } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/resources.go b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/resources.go index d41daad3..dc235699 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/resources.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/resources.go @@ -4,13 +4,12 @@ import ( "context" "fmt" - c1zpb "github.com/conductorone/baton-sdk/pb/c1/c1z/v1" - "github.com/conductorone/baton-sdk/pkg/annotations" "github.com/doug-martin/goqu/v9" - "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" - "go.uber.org/zap" "google.golang.org/protobuf/proto" + c1zpb "github.com/conductorone/baton-sdk/pb/c1/c1z/v1" + "github.com/conductorone/baton-sdk/pkg/annotations" + v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" reader_v2 "github.com/conductorone/baton-sdk/pb/c1/reader/v2" ) @@ -57,8 +56,6 @@ func (r *resourcesTable) Schema() (string, []interface{}) { } func (c *C1File) ListResources(ctx context.Context, request *v2.ResourcesServiceListResourcesRequest) (*v2.ResourcesServiceListResourcesResponse, error) { - ctxzap.Extract(ctx).Debug("listing resources") - objs, nextPageToken, err := c.listConnectorObjects(ctx, resources.Name(), request) if err != nil { return nil, fmt.Errorf("error listing resources: %w", err) @@ -81,12 +78,6 @@ func (c *C1File) ListResources(ctx context.Context, request *v2.ResourcesService } func (c *C1File) GetResource(ctx context.Context, request *reader_v2.ResourcesReaderServiceGetResourceRequest) (*reader_v2.ResourcesReaderServiceGetResourceResponse, error) { - ctxzap.Extract(ctx).Debug( - "fetching resource", - zap.String("resource_id", request.ResourceId.Resource), - zap.String("resource_type_id", request.ResourceId.ResourceType), - ) - ret := &v2.Resource{} annos := annotations.Annotations(request.GetAnnotations()) syncDetails := &c1zpb.SyncDetails{} @@ -106,34 +97,31 @@ func (c *C1File) GetResource(ctx context.Context, request *reader_v2.ResourcesRe }, nil } -func (c *C1File) PutResource(ctx context.Context, resource *v2.Resource) error { - ctxzap.Extract(ctx).Debug( - "syncing resource", - zap.String("resource_id", resource.Id.Resource), - zap.String("resource_type_id", resource.Id.ResourceType), - ) - - updateRecord := goqu.Record{ - "resource_type_id": resource.Id.ResourceType, - "external_id": fmt.Sprintf("%s:%s", resource.Id.ResourceType, resource.Id.Resource), - } - - if resource.ParentResourceId != nil { - updateRecord["parent_resource_type_id"] = resource.ParentResourceId.ResourceType - updateRecord["parent_resource_id"] = resource.ParentResourceId.Resource - } - - query, args, err := c.putConnectorObjectQuery(ctx, resources.Name(), resource, updateRecord) - if err != nil { - return err - } - - _, err = c.db.ExecContext(ctx, query, args...) +func (c *C1File) PutResources(ctx context.Context, resourceObjs ...*v2.Resource) error { + err := c.db.WithTx(func(tx *goqu.TxDatabase) error { + err := bulkPutConnectorObjectTx(ctx, c, tx, resources.Name(), + func(resource *v2.Resource) (goqu.Record, error) { + fields := goqu.Record{ + "resource_type_id": resource.Id.ResourceType, + "external_id": fmt.Sprintf("%s:%s", resource.Id.ResourceType, resource.Id.Resource), + } + + if resource.ParentResourceId != nil { + fields["parent_resource_type_id"] = resource.ParentResourceId.ResourceType + fields["parent_resource_id"] = resource.ParentResourceId.Resource + } + return fields, nil + }, + resourceObjs..., + ) + if err != nil { + return err + } + return nil + }) if err != nil { return err } - c.dbUpdated = true - return nil } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/sql_helpers.go b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/sql_helpers.go index 63f8356e..3bd3fb99 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/sql_helpers.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/sql_helpers.go @@ -70,8 +70,8 @@ type protoHasID interface { GetId() string } -// listConnectorObjects uses a connecter list request to fetch the corresponding data from the local db. -// It returns the raw bytes that need to be unmarshaled into the correct proto message. +// listConnectorObjects uses a connector list request to fetch the corresponding data from the local db. +// It returns the raw bytes that need to be unmarshalled into the correct proto message. func (c *C1File) listConnectorObjects(ctx context.Context, tableName string, req proto.Message) ([][]byte, string, error) { err := c.validateDb(ctx) if err != nil { @@ -231,37 +231,56 @@ func (c *C1File) listConnectorObjects(ctx context.Context, tableName string, req return ret, nextPageToken, nil } -func (c *C1File) putConnectorObjectQuery(ctx context.Context, tableName string, m proto.Message, fields goqu.Record) (string, []interface{}, error) { - err := c.validateSyncDb(ctx) - if err != nil { - return "", nil, err - } +var protoMarshaler = proto.MarshalOptions{Deterministic: true} - messageBlob, err := proto.MarshalOptions{Deterministic: true}.Marshal(m) +func bulkPutConnectorObjectTx[T proto.Message](ctx context.Context, c *C1File, + tx *goqu.TxDatabase, + tableName string, + extractFields func(m T) (goqu.Record, error), + msgs ...T) error { + err := c.validateSyncDb(ctx) if err != nil { - return "", nil, err + return err } - if fields == nil { - fields = goqu.Record{} - } + baseQ := tx.Insert(tableName).Prepared(true) + baseQ = baseQ.OnConflict(goqu.DoUpdate("external_id, sync_id", goqu.C("data").Set(goqu.I("EXCLUDED.data")))) - if _, idSet := fields["external_id"]; !idSet { - idGetter, ok := m.(protoHasID) - if !ok { - return "", nil, fmt.Errorf("unable to get ID for object") + for _, m := range msgs { + messageBlob, err := protoMarshaler.Marshal(m) + if err != nil { + return err } - fields["external_id"] = idGetter.GetId() - } - fields["data"] = messageBlob - fields["sync_id"] = c.currentSyncID - fields["discovered_at"] = time.Now().Format("2006-01-02 15:04:05.999999999") - q := c.db.Insert(tableName).Prepared(true) - q = q.Rows(fields) - q = q.OnConflict(goqu.DoUpdate("external_id, sync_id", goqu.C("data").Set(goqu.I("EXCLUDED.data")))) + fields, err := extractFields(m) + if err != nil { + return err + } + if fields == nil { + fields = goqu.Record{} + } - return q.ToSQL() + if _, idSet := fields["external_id"]; !idSet { + idGetter, ok := any(m).(protoHasID) + if !ok { + return fmt.Errorf("unable to get ID for object") + } + fields["external_id"] = idGetter.GetId() + } + fields["data"] = messageBlob + fields["sync_id"] = c.currentSyncID + fields["discovered_at"] = time.Now().Format("2006-01-02 15:04:05.999999999") + q := baseQ.Rows(fields) + query, args, err := q.ToSQL() + if err != nil { + return err + } + _, err = tx.Exec(query, args...) + if err != nil { + return err + } + } + return nil } func (c *C1File) getResourceObject(ctx context.Context, resourceID *v2.ResourceId, m *v2.Resource, syncID string) error { diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/sync_runs.go b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/sync_runs.go index 9bd1eb64..ed0792f7 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/sync_runs.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/sync_runs.go @@ -299,29 +299,15 @@ func (c *C1File) StartSync(ctx context.Context) (string, bool, error) { return "", false, err } - syncID := ksuid.New().String() + var syncID string if sync != nil && sync.EndedAt == nil { syncID = sync.ID } else { - q := c.db.Insert(syncRuns.Name()) - q = q.Rows(goqu.Record{ - "sync_id": syncID, - "started_at": time.Now().Format("2006-01-02 15:04:05.999999999"), - "sync_token": "", - }) - - query, args, err := q.ToSQL() - if err != nil { - return "", false, err - } - - _, err = c.db.ExecContext(ctx, query, args...) + syncID, err = c.StartNewSync(ctx) if err != nil { return "", false, err } - newSync = true - c.dbUpdated = true } c.currentSyncID = syncID @@ -329,6 +315,37 @@ func (c *C1File) StartSync(ctx context.Context) (string, bool, error) { return c.currentSyncID, newSync, nil } +func (c *C1File) StartNewSync(ctx context.Context) (string, error) { + // Not sure if we want to do this here + if c.currentSyncID != "" { + return c.currentSyncID, nil + } + + syncID := ksuid.New().String() + + q := c.db.Insert(syncRuns.Name()) + q = q.Rows(goqu.Record{ + "sync_id": syncID, + "started_at": time.Now().Format("2006-01-02 15:04:05.999999999"), + "sync_token": "", + }) + + query, args, err := q.ToSQL() + if err != nil { + return "", err + } + + _, err = c.db.ExecContext(ctx, query, args...) + if err != nil { + return "", err + } + + c.dbUpdated = true + c.currentSyncID = syncID + + return c.currentSyncID, nil +} + func (c *C1File) CurrentSyncStep(ctx context.Context) (string, error) { sr, err := c.getCurrentSync(ctx) if err != nil { diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/field/default_relationships.go b/vendor/github.com/conductorone/baton-sdk/pkg/field/default_relationships.go new file mode 100644 index 00000000..f889a02f --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/field/default_relationships.go @@ -0,0 +1,32 @@ +package field + +var defaultRelationship = []SchemaFieldRelationship{ + FieldsRequiredTogether(grantEntitlementField, grantPrincipalField), + FieldsRequiredTogether(clientIDField, clientSecretField), + FieldsRequiredTogether(createTicketField, ticketTemplatePathField), + FieldsRequiredTogether(getTicketField, ticketIDField), + FieldsMutuallyExclusive( + grantEntitlementField, + revokeGrantField, + createAccountLoginField, + deleteResourceField, + rotateCredentialsField, + eventFeedField, + createTicketField, + getTicketField, + ListTicketSchemasField, + ), + FieldsMutuallyExclusive( + grantEntitlementField, + revokeGrantField, + createAccountEmailField, + deleteResourceTypeField, + rotateCredentialsTypeField, + eventFeedField, + ListTicketSchemasField, + ), +} + +func EnsureDefaultRelationships(original []SchemaFieldRelationship) []SchemaFieldRelationship { + return append(defaultRelationship, original...) +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/field/defaults.go b/vendor/github.com/conductorone/baton-sdk/pkg/field/defaults.go new file mode 100644 index 00000000..84468e52 --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/field/defaults.go @@ -0,0 +1,94 @@ +package field + +import "github.com/conductorone/baton-sdk/pkg/logging" + +var ( + createTicketField = BoolField("create-ticket", WithHidden(true), WithDescription("Create ticket")) + getTicketField = BoolField("get-ticket", WithHidden(true), WithDescription("Get ticket")) + ListTicketSchemasField = BoolField("list-ticket-schemas", WithHidden(true), WithDescription("List ticket schemas")) + provisioningField = BoolField("provisioning", WithShortHand("p"), WithDescription("This must be set in order for provisioning actions to be enabled")) + TicketingField = BoolField("ticketing", WithDescription("This must be set to enable ticketing support")) + c1zTmpDirField = StringField("c1z-temp-dir", WithHidden(true), WithDescription("The directory to store temporary files in. It must exist, "+ + "and write access is required. Defaults to the OS temporary directory.")) + clientIDField = StringField("client-id", WithDescription("The client ID used to authenticate with ConductorOne")) + clientSecretField = StringField("client-secret", WithDescription("The client secret used to authenticate with ConductorOne")) + createAccountEmailField = StringField("create-account-email", WithHidden(true), WithDescription("The email of the account to create")) + createAccountLoginField = StringField("create-account-login", WithHidden(true), WithDescription("The login of the account to create")) + deleteResourceField = StringField("delete-resource", WithHidden(true), WithDescription("The id of the resource to delete")) + deleteResourceTypeField = StringField("delete-resource-type", WithHidden(true), WithDescription("The type of the resource to delete")) + eventFeedField = StringField("event-feed", WithHidden(true), WithDescription("Read feed events to stdout")) + fileField = StringField("file", WithShortHand("f"), WithDefaultValue("sync.c1z"), WithDescription("The path to the c1z file to sync with")) + grantEntitlementField = StringField("grant-entitlement", WithHidden(true), WithDescription("The id of the entitlement to grant to the supplied principal")) + grantPrincipalField = StringField("grant-principal", WithHidden(true), WithDescription("The id of the resource to grant the entitlement to")) + grantPrincipalTypeField = StringField("grant-principal-type", WithHidden(true), WithDescription("The resource type of the principal to grant the entitlement to")) + logFormatField = StringField("log-format", WithDefaultValue(logging.LogFormatJSON), WithDescription("The output format for logs: json, console")) + revokeGrantField = StringField("revoke-grant", WithHidden(true), WithDescription("The grant to revoke")) + rotateCredentialsField = StringField("rotate-credentials", WithHidden(true), WithDescription("The id of the resource to rotate credentials on")) + rotateCredentialsTypeField = StringField("rotate-credentials-type", WithHidden(true), WithDescription("The type of the resource to rotate credentials on")) + ticketIDField = StringField("ticket-id", WithHidden(true), WithDescription("The ID of the ticket to get")) + ticketTemplatePathField = StringField("ticket-template-path", WithHidden(true), WithDescription("A JSON file describing the ticket to create")) + logLevelField = StringField("log-level", WithDefaultValue("info"), WithDescription("The log level: debug, info, warn, error")) + skipFullSync = BoolField("skip-full-sync", WithDescription("This must be set to skip a full sync")) +) + +// DefaultFields list the default fields expected in every single connector. +var DefaultFields = []SchemaField{ + createTicketField, + getTicketField, + ListTicketSchemasField, + provisioningField, + TicketingField, + c1zTmpDirField, + clientIDField, + clientSecretField, + createAccountEmailField, + createAccountLoginField, + deleteResourceField, + deleteResourceTypeField, + eventFeedField, + fileField, + grantEntitlementField, + grantPrincipalField, + grantPrincipalTypeField, + logFormatField, + revokeGrantField, + rotateCredentialsField, + rotateCredentialsTypeField, + ticketIDField, + ticketTemplatePathField, + logLevelField, + skipFullSync, +} + +func IsFieldAmongDefaultList(f SchemaField) bool { + for _, v := range DefaultFields { + if v.FieldName == f.FieldName { + return true + } + } + + return false +} + +func EnsureDefaultFieldsExists(originalFields []SchemaField) []SchemaField { + var notfound []SchemaField + + // compare the default list of fields + // with the incoming original list of fields + for _, d := range DefaultFields { + found := false + for _, o := range originalFields { + if d.FieldName == o.FieldName { + found = true + } + } + + if !found { + notfound = append(notfound, d) + } + } + + notfound = append(notfound, originalFields...) + + return notfound +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/field/field_options.go b/vendor/github.com/conductorone/baton-sdk/pkg/field/field_options.go new file mode 100644 index 00000000..7177ee06 --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/field/field_options.go @@ -0,0 +1,42 @@ +package field + +type fieldOption func(SchemaField) SchemaField + +func WithRequired(required bool) fieldOption { + return func(o SchemaField) SchemaField { + o.Required = required + return o + } +} + +func WithDescription(description string) fieldOption { + return func(o SchemaField) SchemaField { + o.Description = description + + return o + } +} + +func WithDefaultValue(value any) fieldOption { + return func(o SchemaField) SchemaField { + o.DefaultValue = value + + return o + } +} + +func WithHidden(hidden bool) fieldOption { + return func(o SchemaField) SchemaField { + o.Hidden = hidden + + return o + } +} + +func WithShortHand(sh string) fieldOption { + return func(o SchemaField) SchemaField { + o.CLIShortHand = sh + + return o + } +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/field/fields.go b/vendor/github.com/conductorone/baton-sdk/pkg/field/fields.go new file mode 100644 index 00000000..4413407e --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/field/fields.go @@ -0,0 +1,149 @@ +package field + +import ( + "errors" + "fmt" + "reflect" + "strings" +) + +var ( + WrongValueTypeErr = errors.New("unable to cast any to concrete type") +) + +type SchemaField struct { + FieldName string + FieldType reflect.Kind + CLIShortHand string + Required bool + Hidden bool + Description string + DefaultValue any +} + +// Bool returns the default value as a boolean. +func (s SchemaField) Bool() (bool, error) { + value, ok := s.DefaultValue.(bool) + if !ok { + return false, WrongValueTypeErr + } + + return value, nil +} + +// Int returns the default value as a integer. +func (s SchemaField) Int() (int, error) { + value, ok := s.DefaultValue.(int) + if !ok { + return 0, WrongValueTypeErr + } + + return value, nil +} + +// String returns the default value as a string. +func (s SchemaField) String() (string, error) { + value, ok := s.DefaultValue.(string) + if !ok { + return "", WrongValueTypeErr + } + + return value, nil +} + +// StringSlice retuns the default value as a string array. +func (s SchemaField) StringSlice() ([]string, error) { + value, ok := s.DefaultValue.([]string) + if !ok { + return nil, WrongValueTypeErr + } + + return value, nil +} + +func (s SchemaField) GetDescription() string { + var line string + if s.Description == "" { + line = fmt.Sprintf("($BATON_%s)", toUpperCase(s.FieldName)) + } else { + line = fmt.Sprintf("%s ($BATON_%s)", s.Description, toUpperCase(s.FieldName)) + } + + if s.Required { + line = fmt.Sprintf("required: %s", line) + } + + return line +} + +func (s SchemaField) GetName() string { + return s.FieldName +} + +func (s SchemaField) GetType() reflect.Kind { + return s.FieldType +} + +func BoolField(name string, optional ...fieldOption) SchemaField { + field := SchemaField{ + FieldName: name, + FieldType: reflect.Bool, + DefaultValue: false, + } + + for _, o := range optional { + field = o(field) + } + + if field.Required { + panic(fmt.Sprintf("requiring %s of type %s does not make sense", field.FieldName, field.FieldType)) + } + + return field +} + +func StringField(name string, optional ...fieldOption) SchemaField { + field := SchemaField{ + FieldName: name, + FieldType: reflect.String, + DefaultValue: "", + } + + for _, o := range optional { + field = o(field) + } + + return field +} + +func IntField(name string, optional ...fieldOption) SchemaField { + field := SchemaField{ + FieldName: name, + FieldType: reflect.Int, + DefaultValue: 0, + } + + for _, o := range optional { + field = o(field) + } + + return field +} + +func StringSliceField(name string, optional ...fieldOption) SchemaField { + field := SchemaField{ + FieldName: name, + FieldType: reflect.Slice, + DefaultValue: []string{}, + } + + for _, o := range optional { + field = o(field) + } + + return field +} + +func toUpperCase(i string) string { + return strings.ReplaceAll(strings.ToUpper(i), "-", "_") +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/field/relationships.go b/vendor/github.com/conductorone/baton-sdk/pkg/field/relationships.go new file mode 100644 index 00000000..769f822c --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/field/relationships.go @@ -0,0 +1,45 @@ +package field + +type Relationship int + +const ( + RequiredTogether Relationship = iota + 1 + MutuallyExclusive + AtLeastOne + Dependents +) + +type SchemaFieldRelationship struct { + Kind Relationship + Fields []SchemaField + ExpectedFields []SchemaField +} + +func FieldsRequiredTogether(fields ...SchemaField) SchemaFieldRelationship { + return SchemaFieldRelationship{ + Kind: RequiredTogether, + Fields: fields, + } +} + +func FieldsMutuallyExclusive(fields ...SchemaField) SchemaFieldRelationship { + return SchemaFieldRelationship{ + Kind: MutuallyExclusive, + Fields: fields, + } +} + +func FieldsAtLeastOneUsed(fields ...SchemaField) SchemaFieldRelationship { + return SchemaFieldRelationship{ + Kind: AtLeastOne, + Fields: fields, + } +} + +func FieldsDependentOn(dependent []SchemaField, expected []SchemaField) SchemaFieldRelationship { + return SchemaFieldRelationship{ + Kind: Dependents, + Fields: dependent, + ExpectedFields: expected, + } +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/field/struct.go b/vendor/github.com/conductorone/baton-sdk/pkg/field/struct.go new file mode 100644 index 00000000..12ecf64d --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/field/struct.go @@ -0,0 +1,13 @@ +package field + +type Configuration struct { + Fields []SchemaField + Constraints []SchemaFieldRelationship +} + +func NewConfiguration(fields []SchemaField, constraints ...SchemaFieldRelationship) Configuration { + return Configuration{ + Fields: fields, + Constraints: constraints, + } +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/field/validation.go b/vendor/github.com/conductorone/baton-sdk/pkg/field/validation.go new file mode 100644 index 00000000..05812b57 --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/field/validation.go @@ -0,0 +1,153 @@ +package field + +import ( + "fmt" + "reflect" + "strings" + + "github.com/spf13/viper" +) + +type ErrConfigurationMissingFields struct { + errors []error +} + +func (e *ErrConfigurationMissingFields) Error() string { + var messages []string + + for _, err := range e.errors { + messages = append(messages, err.Error()) + } + + return fmt.Sprintf("errors found:\n%s", strings.Join(messages, "\n")) +} + +func (e *ErrConfigurationMissingFields) Push(err error) { + e.errors = append(e.errors, err) +} + +// Validate perform validation of field requirement and constraints +// relationships after the configuration is read. +// We don't check the following: +// - if required fields are mutually exclusive +// - repeated fields (by name) are defined +// - if sets of fields are mutually exclusive and required +// together at the same time +// - if fields depedent on themselves +func Validate(c Configuration, v *viper.Viper) error { + present := make(map[string]int) + missingFieldsError := &ErrConfigurationMissingFields{} + + // check if required fields are present + for _, f := range c.Fields { + isNonZero := false + switch f.FieldType { + case reflect.Bool: + isNonZero = v.GetBool(f.FieldName) + case reflect.Int: + isNonZero = v.GetInt(f.FieldName) != 0 + case reflect.String: + isNonZero = v.GetString(f.FieldName) != "" + case reflect.Slice: + isNonZero = len(v.GetStringSlice(f.FieldName)) != 0 + default: + return fmt.Errorf("field %s has unsupported type %s", f.FieldName, f.FieldType) + } + + if isNonZero { + present[f.FieldName] = 1 + } + + if f.Required && !isNonZero { + missingFieldsError.Push(fmt.Errorf("field %s of type %s is marked as required but it has a zero-value", f.FieldName, f.FieldType)) + } + } + + if len(missingFieldsError.errors) > 0 { + return missingFieldsError + } + + // check constraints + return validateConstraints(present, c.Constraints) +} + +func validateConstraints(fieldsPresent map[string]int, relationships []SchemaFieldRelationship) error { + for _, relationship := range relationships { + var present int + for _, f := range relationship.Fields { + present += fieldsPresent[f.FieldName] + } + + var expected int + for _, e := range relationship.ExpectedFields { + expected += fieldsPresent[e.FieldName] + } + + if present > 1 && relationship.Kind == MutuallyExclusive { + return makeMutuallyExclusiveError(fieldsPresent, relationship) + } + if present > 0 && present < len(relationship.Fields) && relationship.Kind == RequiredTogether { + return makeNeededTogetherError(fieldsPresent, relationship) + } + if present == 0 && relationship.Kind == AtLeastOne { + return makeAtLeastOneError(fieldsPresent, relationship) + } + if present > 0 && expected != len(relationship.ExpectedFields) && relationship.Kind == Dependents { + return makeDependentFieldsError(fieldsPresent, relationship) + } + } + + return nil +} + +func makeMutuallyExclusiveError(fields map[string]int, relation SchemaFieldRelationship) error { + var found []string + for _, f := range relation.Fields { + if fields[f.FieldName] == 1 { + found = append(found, f.FieldName) + } + } + + return fmt.Errorf("fields marked as mutually exclusive were set: %s", strings.Join(found, ", ")) +} + +func makeNeededTogetherError(fields map[string]int, relation SchemaFieldRelationship) error { + var found []string + for _, f := range relation.Fields { + if fields[f.FieldName] == 0 { + found = append(found, f.FieldName) + } + } + + return fmt.Errorf("fields marked as needed together are missing: %s", strings.Join(found, ", ")) +} + +func makeAtLeastOneError(fields map[string]int, relation SchemaFieldRelationship) error { + var found []string + for _, f := range relation.Fields { + if fields[f.FieldName] == 0 { + found = append(found, f.FieldName) + } + } + + return fmt.Errorf("at least one field was expected, any of: %s", strings.Join(found, ", ")) +} + +func makeDependentFieldsError(fields map[string]int, relation SchemaFieldRelationship) error { + var notfoundExpected []string + for _, n := range relation.ExpectedFields { + if fields[n.FieldName] == 0 { + notfoundExpected = append(notfoundExpected, n.FieldName) + } + } + + var foundDependent []string + for _, f := range relation.Fields { + if fields[f.FieldName] == 1 { + foundDependent = append(foundDependent, f.FieldName) + } + } + + return fmt.Errorf("set fields %s are dependent on %s being set", + strings.Join(foundDependent, ", "), strings.Join(notfoundExpected, ", ")) +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/helpers/helpers.go b/vendor/github.com/conductorone/baton-sdk/pkg/helpers/helpers.go index b08f6899..b4da5ed8 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/helpers/helpers.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/helpers/helpers.go @@ -25,6 +25,57 @@ func SplitFullName(name string) (string, string) { return firstName, lastName } +var limitHeaders = []string{ + "X-Ratelimit-Limit", + "Ratelimit-Limit", + "X-RateLimit-Requests-Limit", // Linear uses a non-standard header +} + +var remainingHeaders = []string{ + "X-Ratelimit-Remaining", + "Ratelimit-Remaining", + "X-RateLimit-Requests-Remaining", // Linear uses a non-standard header +} + +var resetAtHeaders = []string{ + "X-Ratelimit-Reset", + "Ratelimit-Reset", + "X-RateLimit-Requests-Reset", // Linear uses a non-standard header + "Retry-After", // Often returned with 429 +} + +const thirtyYears = 60 * 60 * 24 * 365 * (2000 - 1970) + +// Many APIs don't follow standards and return incorrect datetimes. This function tries to handle those cases. +func parseTime(timeStr string) (time.Time, error) { + var t time.Time + res, err := strconv.ParseInt(timeStr, 10, 64) + if err != nil { + t, err = time.Parse(time.RFC850, timeStr) + if err != nil { + // Datetimes should be RFC850 but some APIs return RFC3339 + t, err = time.Parse(time.RFC3339, timeStr) + } + return t, err + } + + // Times are supposed to be in seconds, but some APIs return milliseconds + if res > thirtyYears*1000 { + res /= 1000 + } + + // Times are supposed to be offsets, but some return absolute seconds since 1970. + if res > thirtyYears { + // If more than 30 years, it's probably an absolute timestamp + t = time.Unix(res, 0) + } else { + // Otherwise, it's a relative timestamp + t = time.Now().Add(time.Second * time.Duration(res)) + } + + return t, nil +} + func ExtractRateLimitData(statusCode int, header *http.Header) (*v2.RateLimitDescription, error) { if header == nil { return nil, nil @@ -34,43 +85,53 @@ func ExtractRateLimitData(statusCode int, header *http.Header) (*v2.RateLimitDes var limit int64 var err error - limitStr := header.Get("X-Ratelimit-Limit") - if limitStr != "" { - limit, err = strconv.ParseInt(limitStr, 10, 64) - if err != nil { - return nil, err + for _, limitHeader := range limitHeaders { + limitStr := header.Get(limitHeader) + if limitStr != "" { + limit, err = strconv.ParseInt(limitStr, 10, 64) + if err != nil { + return nil, err + } + break } } var remaining int64 - remainingStr := header.Get("X-Ratelimit-Remaining") - if remainingStr != "" { - remaining, err = strconv.ParseInt(remainingStr, 10, 64) - if err != nil { - return nil, err - } - if remaining > 0 { - rlstatus = v2.RateLimitDescription_STATUS_OK + for _, remainingHeader := range remainingHeaders { + remainingStr := header.Get(remainingHeader) + if remainingStr != "" { + remaining, err = strconv.ParseInt(remainingStr, 10, 64) + if err != nil { + return nil, err + } + break } } + if remaining > 0 { + rlstatus = v2.RateLimitDescription_STATUS_OK + } var resetAt time.Time - reset := header.Get("X-Ratelimit-Reset") - if reset != "" { - res, err := strconv.ParseInt(reset, 10, 64) - if err != nil { - return nil, err + for _, resetAtHeader := range resetAtHeaders { + resetAtStr := header.Get(resetAtHeader) + if resetAtStr != "" { + resetAt, err = parseTime(resetAtStr) + if err != nil { + return nil, err + } + break } + } - resetAt = time.Now().Add(time.Second * time.Duration(res)) + if statusCode == http.StatusTooManyRequests { + rlstatus = v2.RateLimitDescription_STATUS_OVERLIMIT + remaining = 0 } // If we didn't get any rate limit headers and status code is 429, return some sane defaults - if limit == 0 && remaining == 0 && resetAt.IsZero() && statusCode == http.StatusTooManyRequests { + if remaining == 0 && resetAt.IsZero() && rlstatus == v2.RateLimitDescription_STATUS_OVERLIMIT { limit = 1 - remaining = 0 resetAt = time.Now().Add(time.Second * 60) - rlstatus = v2.RateLimitDescription_STATUS_OVERLIMIT } return &v2.RateLimitDescription{ @@ -103,7 +164,7 @@ func IsXMLContentType(contentType string) bool { normalizedContentType := strings.TrimSpace(strings.ToLower(contentType)) for _, xmlContentType := range xmlContentTypes { - if normalizedContentType == xmlContentType { + if strings.HasPrefix(normalizedContentType, xmlContentType) { return true } } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/metrics/instrumentor.go b/vendor/github.com/conductorone/baton-sdk/pkg/metrics/instrumentor.go index 81025f5f..52541d43 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/metrics/instrumentor.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/metrics/instrumentor.go @@ -8,9 +8,9 @@ import ( ) const ( - taskSuccessCounterName = "task_success" - taskFailureCounterName = "task_failure" - taskDurationHistoName = "task_latency" + taskSuccessCounterName = "baton_sdk.task_success" + taskFailureCounterName = "baton_sdk.task_failure" + taskDurationHistoName = "baton_sdk.task_latency" taskSuccessCounterDesc = "number of successful tasks by task type" taskFailureCounterDesc = "number of failed tasks by task type" taskDurationHistoDesc = "duration of all tasks by task type and status" diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/metrics/otel.go b/vendor/github.com/conductorone/baton-sdk/pkg/metrics/otel.go index 768ab1aa..9a43d79e 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/metrics/otel.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/metrics/otel.go @@ -9,60 +9,77 @@ import ( otelmetric "go.opentelemetry.io/otel/metric" ) +var ( + _ Handler = (*otelHandler)(nil) + _ Int64Counter = (*otelInt64Counter)(nil) + _ Int64Gauge = (*otelInt64Gauge)(nil) + _ Int64Histogram = (*otelInt64Histogram)(nil) +) + type otelHandler struct { - name string - meter otelmetric.Meter - provider otelmetric.MeterProvider + name string + meter otelmetric.Meter + provider otelmetric.MeterProvider + defaultAttrs *[]attribute.KeyValue int64CountersMtx sync.Mutex - int64Counters map[string]otelmetric.Int64Counter + int64Counters map[string]*otelInt64Counter int64HistosMtx sync.Mutex - int64Histos map[string]otelmetric.Int64Histogram + int64Histos map[string]*otelInt64Histogram int64GaugesMtx sync.Mutex - int64Gauges map[string]Int64Gauge + int64Gauges map[string]*otelInt64Gauge } -type otelInt64Histogram func(ctx context.Context, incr int64, options ...otelmetric.RecordOption) +type baseAttrs struct { + defaultAttrs *[]attribute.KeyValue +} -func (f otelInt64Histogram) Record(ctx context.Context, value int64, tags map[string]string) { +func (a *baseAttrs) getAttributes(tags map[string]string) []attribute.KeyValue { attrs := makeAttrs(tags) + if a.defaultAttrs != nil { + attrs = append(attrs, *a.defaultAttrs...) + } - f(ctx, value, otelmetric.WithAttributes(attrs...)) + return attrs } -var _ Int64Histogram = (otelInt64Histogram)(nil) - -type otelInt64Counter func(ctx context.Context, incr int64, options ...otelmetric.AddOption) +func (a *baseAttrs) setDefaultAttrs(attrs *[]attribute.KeyValue) { + a.defaultAttrs = attrs +} -func (f otelInt64Counter) Add(ctx context.Context, value int64, tags map[string]string) { - attrs := makeAttrs(tags) - f(ctx, value, otelmetric.WithAttributes(attrs...)) +type otelInt64Counter struct { + *baseAttrs + counter otelmetric.Int64Counter } -var _ Int64Counter = (otelInt64Counter)(nil) +func (c *otelInt64Counter) Add(ctx context.Context, value int64, tags map[string]string) { + attrs := c.getAttributes(tags) -type syncInt64Gauge struct { - value int64 - attrs []otelmetric.ObserveOption - gauge otelmetric.Int64ObservableGauge + c.counter.Add(ctx, value, otelmetric.WithAttributes(attrs...)) } -func (s *syncInt64Gauge) Observe(_ context.Context, value int64, tags map[string]string) { - attrs := makeAttrs(tags) - s.attrs = append(s.attrs, otelmetric.WithAttributes(attrs...)) - s.value = value +type otelInt64Histogram struct { + *baseAttrs + histo otelmetric.Int64Histogram } -func newSyncInt64Gauge(meter otelmetric.Meter, name string, description string, unit Unit) *syncInt64Gauge { - g, err := meter.Int64ObservableGauge(name, otelmetric.WithDescription(description), otelmetric.WithUnit(string(unit))) - if err != nil { - panic(err) - } +func (h *otelInt64Histogram) Record(ctx context.Context, value int64, tags map[string]string) { + attrs := h.getAttributes(tags) + + h.histo.Record(ctx, value, otelmetric.WithAttributes(attrs...)) +} - return &syncInt64Gauge{gauge: g} +type otelInt64Gauge struct { + *baseAttrs + value int64 + attrs []attribute.KeyValue + gauge otelmetric.Int64ObservableGauge } -var _ Int64Gauge = (*syncInt64Gauge)(nil) +func (g *otelInt64Gauge) Observe(_ context.Context, value int64, tags map[string]string) { + g.attrs = g.getAttributes(tags) + g.value = value +} func (h *otelHandler) Int64Histogram(name string, description string, unit Unit) Int64Histogram { h.int64HistosMtx.Lock() @@ -71,16 +88,18 @@ func (h *otelHandler) Int64Histogram(name string, description string, unit Unit) name = strings.ToLower(name) c, ok := h.int64Histos[name] - var err error if !ok { - c, err = h.meter.Int64Histogram(name, otelmetric.WithDescription(description), otelmetric.WithUnit(string(unit))) + histo, err := h.meter.Int64Histogram(name, otelmetric.WithDescription(description), otelmetric.WithUnit(string(unit))) if err != nil { panic(err) } + c = &otelInt64Histogram{histo: histo, baseAttrs: &baseAttrs{}} h.int64Histos[name] = c } - return otelInt64Histogram(c.Record) + c.setDefaultAttrs(h.defaultAttrs) + + return c } func (h *otelHandler) Int64Counter(name string, description string, unit Unit) Int64Counter { @@ -90,16 +109,18 @@ func (h *otelHandler) Int64Counter(name string, description string, unit Unit) I name = strings.ToLower(name) c, ok := h.int64Counters[name] - var err error if !ok { - c, err = h.meter.Int64Counter(name, otelmetric.WithDescription(description), otelmetric.WithUnit(string(unit))) + counter, err := h.meter.Int64Counter(name, otelmetric.WithDescription(description), otelmetric.WithUnit(string(unit))) if err != nil { panic(err) } + c = &otelInt64Counter{counter: counter, baseAttrs: &baseAttrs{}} h.int64Counters[name] = c } - return otelInt64Counter(c.Add) + c.setDefaultAttrs(h.defaultAttrs) + + return c } func (h *otelHandler) Int64Gauge(name string, description string, unit Unit) Int64Gauge { @@ -108,36 +129,41 @@ func (h *otelHandler) Int64Gauge(name string, description string, unit Unit) Int name = strings.ToLower(name) - if c, ok := h.int64Gauges[name]; ok { - return c - } + c, ok := h.int64Gauges[name] + if !ok { + gauge, err := h.meter.Int64ObservableGauge(name, otelmetric.WithDescription(description), otelmetric.WithUnit(string(unit))) + if err != nil { + panic(err) + } - newGauge := newSyncInt64Gauge(h.meter, name, description, unit) + c = &otelInt64Gauge{gauge: gauge, baseAttrs: &baseAttrs{}} - _, err := h.meter.RegisterCallback(func(ctx context.Context, observer otelmetric.Observer) error { - observer.ObserveInt64(newGauge.gauge, newGauge.value, newGauge.attrs...) - return nil - }, newGauge.gauge) + _, err = h.meter.RegisterCallback(func(ctx context.Context, observer otelmetric.Observer) error { + observer.ObserveInt64(c.gauge, c.value, otelmetric.WithAttributes(c.attrs...)) + return nil + }, c.gauge) + if err != nil { + panic(err) + } - if err != nil { - panic(err) + h.int64Gauges[name] = c } - h.int64Gauges[name] = newGauge + c.setDefaultAttrs(h.defaultAttrs) - return newGauge + return c } func (h *otelHandler) WithTags(tags map[string]string) Handler { attrs := makeAttrs(tags) - h.meter = h.provider.Meter(h.name, otelmetric.WithInstrumentationAttributes(attrs...)) + h.defaultAttrs = &attrs return h } func makeAttrs(tags map[string]string) []attribute.KeyValue { - attrs := make([]attribute.KeyValue, len(tags)) + attrs := make([]attribute.KeyValue, 0, len(tags)) for k, v := range tags { attrs = append(attrs, attribute.String(k, v)) } @@ -150,10 +176,8 @@ func NewOtelHandler(_ context.Context, provider otelmetric.MeterProvider, name s name: name, meter: provider.Meter(name), provider: provider, - int64Counters: make(map[string]otelmetric.Int64Counter), - int64Histos: make(map[string]otelmetric.Int64Histogram), - int64Gauges: make(map[string]Int64Gauge), + int64Counters: make(map[string]*otelInt64Counter), + int64Histos: make(map[string]*otelInt64Histogram), + int64Gauges: make(map[string]*otelInt64Gauge), } } - -var _ Handler = (*otelHandler)(nil) diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/pagination/pagination.go b/vendor/github.com/conductorone/baton-sdk/pkg/pagination/pagination.go index 563ac264..74082b65 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/pagination/pagination.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/pagination/pagination.go @@ -21,9 +21,9 @@ type Token struct { } type PageState struct { - Token string `json:"token"` - ResourceTypeID string `json:"resource_type_id"` - ResourceID string `json:"resource_id"` + Token string `json:"token,omitempty"` + ResourceTypeID string `json:"type,omitempty"` + ResourceID string `json:"id,omitempty"` } // Bag holds pagination states that can be serialized for use as page tokens. It acts as a stack that you can push and pop diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/sdk/version.go b/vendor/github.com/conductorone/baton-sdk/pkg/sdk/version.go index f98920c5..6bb54164 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/sdk/version.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/sdk/version.go @@ -1,4 +1,3 @@ package sdk -// Version is the current version of the baton SDK. -const Version = "0.1.30" +const Version = "v0.2.17" diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand.go b/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand.go deleted file mode 100644 index d02fcb6f..00000000 --- a/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand.go +++ /dev/null @@ -1,408 +0,0 @@ -package sync - -import ( - "context" - "errors" - "fmt" - "reflect" - - v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" - "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" - "go.uber.org/zap" -) - -var ( - ErrNoEntitlement = errors.New("no entitlement found") -) - -type EntitlementGraphAction struct { - SourceEntitlementID string `json:"source_entitlement_id"` - DescendantEntitlementID string `json:"descendant_entitlement_id"` - Shallow bool `json:"shallow"` - ResourceTypeIDs []string `json:"resource_types_ids"` - PageToken string `json:"page_token"` -} - -// Edges between entitlements are grants. -type grantInfo struct { - Expanded bool `json:"expanded"` - Shallow bool `json:"shallow"` - ResourceTypeIDs []string `json:"resource_type_ids"` -} - -type Node struct { - Id int `json:"id"` - EntitlementIDs []string `json:"entitlementIds"` // List of entitlements. -} - -type EntitlementGraph struct { - NodeCount int `json:"node_count"` - Nodes map[int]Node `json:"nodes"` - Edges map[int]map[int]*grantInfo `json:"edges"` // Adjacency list. Source node -> destination node - Loaded bool `json:"loaded"` - Depth int `json:"depth"` - Actions []EntitlementGraphAction `json:"actions"` -} - -func NewEntitlementGraph(ctx context.Context) *EntitlementGraph { - return &EntitlementGraph{ - NodeCount: 1, // Start at 1 in case we don't initialize something and try to get node 0 - Nodes: make(map[int]Node), - // TODO: probably want this for efficiency - // EntitlementsToNodes: make(map[string]int), - Edges: make(map[int]map[int]*grantInfo), - } -} - -func (g *EntitlementGraph) Str() string { - str := "\n" - for id, node := range g.Nodes { - str += fmt.Sprintf("node %v entitlement IDs: %v\n", id, node.EntitlementIDs) - } - str += "edges:\n" - for src, dsts := range g.Edges { - for dst, gi := range dsts { - str += fmt.Sprintf("%v -> %v grantInfo %v\n", src, dst, gi) - } - } - return str -} - -func (g *EntitlementGraph) Validate() error { - for srcNodeId, dstNodeIDs := range g.Edges { - node, ok := g.Nodes[srcNodeId] - if !ok { - return ErrNoEntitlement - } - if len(node.EntitlementIDs) == 0 { - return fmt.Errorf("empty node") - } - for dstNodeId, grantInfo := range dstNodeIDs { - node, ok := g.Nodes[dstNodeId] - if !ok { - return ErrNoEntitlement - } - if len(node.EntitlementIDs) == 0 { - return fmt.Errorf("empty node") - } - if grantInfo == nil { - return fmt.Errorf("nil edge info. dst node %v", dstNodeId) - } - } - } - // check for entitlement ids that are in multiple nodes - seenEntitlements := make(map[string]int) - for nodeID, node := range g.Nodes { - for _, entID := range node.EntitlementIDs { - if _, ok := seenEntitlements[entID]; ok { - return fmt.Errorf("entitlement %v is in multiple nodes: %v %v", entID, nodeID, seenEntitlements[entID]) - } - seenEntitlements[entID] = nodeID - } - } - return nil -} - -func (g *EntitlementGraph) isNodeExpanded(nodeID int) bool { - dstNodeIDs := g.Edges[nodeID] - for _, edgeInfo := range dstNodeIDs { - if !edgeInfo.Expanded { - return false - } - } - return true -} - -// IsExpanded returns true if all entitlements in the graph have been expanded. -func (g *EntitlementGraph) IsExpanded() bool { - for srcNodeID := range g.Edges { - if !g.isNodeExpanded(srcNodeID) { - return false - } - } - return true -} - -// IsEntitlementExpanded returns true if all the outgoing edges for the given entitlement have been expanded. -func (g *EntitlementGraph) IsEntitlementExpanded(entitlementID string) bool { - node := g.GetNode(entitlementID) - if node == nil { - // TODO: log error? return error? - return false - } - if !g.isNodeExpanded(node.Id) { - return false - } - return true -} - -// HasUnexpandedAncestors returns true if the given entitlement has ancestors that have not been expanded yet. -func (g *EntitlementGraph) HasUnexpandedAncestors(entitlementID string) bool { - node := g.GetNode(entitlementID) - if node == nil { - return false - } - - for _, ancestorId := range g.getAncestors(entitlementID) { - if !g.isNodeExpanded(ancestorId) { - return true - } - } - return false -} - -func (g *EntitlementGraph) getAncestors(entitlementID string) []int { - node := g.GetNode(entitlementID) - if node == nil { - panic("entitlement not found") - // return nil - } - - ancestors := make([]int, 0) - for src, dst := range g.Edges { - if _, ok := dst[node.Id]; ok { - ancestors = append(ancestors, src) - } - } - return ancestors -} - -// Find the direct ancestors of the given entitlement. The 'all' flag returns all ancestors regardless of 'done' state. -func (g *EntitlementGraph) GetCycles() ([][]int, bool) { - rv := make([][]int, 0) - for nodeID := range g.Nodes { - edges, ok := g.Edges[nodeID] - if !ok || len(edges) == 0 { - continue - } - cycle, isCycle := g.getCycle([]int{nodeID}) - if isCycle && !isInCycle(cycle, rv) { - rv = append(rv, cycle) - } - } - - return rv, len(rv) > 0 -} - -func isInCycle(newCycle []int, cycles [][]int) bool { - for _, cycle := range cycles { - if len(cycle) > 0 && reflect.DeepEqual(cycle, newCycle) { - return true - } - } - return false -} - -func shift(arr []int, n int) []int { - for i := 0; i < n; i++ { - arr = append(arr[1:], arr[0]) - } - return arr -} - -func (g *EntitlementGraph) getCycle(visits []int) ([]int, bool) { - if len(visits) == 0 { - return nil, false - } - nodeId := visits[len(visits)-1] - for descendantId := range g.Edges[nodeId] { - tempVisits := make([]int, len(visits)) - copy(tempVisits, visits) - if descendantId == visits[0] { - // shift array so that the smallest element is first - smallestIndex := 0 - for i := range tempVisits { - if tempVisits[i] < tempVisits[smallestIndex] { - smallestIndex = i - } - } - tempVisits = shift(tempVisits, smallestIndex) - return tempVisits, true - } - for _, visit := range visits { - if visit == descendantId { - return nil, false - } - } - - tempVisits = append(tempVisits, descendantId) - return g.getCycle(tempVisits) - } - return nil, false -} - -func (g *EntitlementGraph) GetDescendantEntitlements(entitlementID string) map[string]*grantInfo { - node := g.GetNode(entitlementID) - if node == nil { - return nil - } - entsToGrants := make(map[string]*grantInfo) - for dstNodeId, edgeInfo := range g.Edges[node.Id] { - dstNode := g.Nodes[dstNodeId] - for _, entId := range dstNode.EntitlementIDs { - entsToGrants[entId] = edgeInfo - } - } - return entsToGrants -} - -func (g *EntitlementGraph) HasEntitlement(entitlementID string) bool { - return g.GetNode(entitlementID) != nil -} - -func (g *EntitlementGraph) AddEntitlement(entitlement *v2.Entitlement) { - node := g.GetNode(entitlement.Id) - if node != nil { - return - } - - g.Nodes[g.NodeCount] = Node{ - Id: g.NodeCount, - EntitlementIDs: []string{entitlement.Id}, - } - g.NodeCount++ -} - -func (g *EntitlementGraph) GetEntitlements() []string { - var entitlements []string - for _, node := range g.Nodes { - entitlements = append(entitlements, node.EntitlementIDs...) - } - return entitlements -} - -func (g *EntitlementGraph) MarkEdgeExpanded(sourceEntitlementID string, descendantEntitlementID string) { - srcNode := g.GetNode(sourceEntitlementID) - if srcNode == nil { - // TODO: panic? - return - } - dstNode := g.GetNode(descendantEntitlementID) - if dstNode == nil { - // TODO: panic? - return - } - _, ok := g.Edges[srcNode.Id][dstNode.Id] - if !ok { - return - } - - g.Edges[srcNode.Id][dstNode.Id].Expanded = true -} - -func (g *EntitlementGraph) GetNode(entitlementId string) *Node { - // TODO: add an EntitlementToNode map for efficiency - for _, node := range g.Nodes { - for _, entId := range node.EntitlementIDs { - if entId == entitlementId { - return &node - } - } - } - return nil -} - -func (g *EntitlementGraph) AddEdge(ctx context.Context, srcEntitlementID string, dstEntitlementID string, shallow bool, resourceTypeIDs []string) error { - srcNode := g.GetNode(srcEntitlementID) - if srcNode == nil { - return ErrNoEntitlement - } - dstNode := g.GetNode(dstEntitlementID) - if dstNode == nil { - return ErrNoEntitlement - } - - _, ok := g.Edges[srcNode.Id] - if !ok { - g.Edges[srcNode.Id] = make(map[int]*grantInfo) - } - - _, ok = g.Edges[srcNode.Id][dstNode.Id] - if !ok { - g.Edges[srcNode.Id][dstNode.Id] = &grantInfo{ - Expanded: false, - Shallow: shallow, - ResourceTypeIDs: resourceTypeIDs, - } - } else { - // TODO: just do nothing? it's probably a mistake if we're adding the same edge twice - ctxzap.Extract(ctx).Warn( - "duplicate edge from datasource", - zap.String("src_entitlement_id", srcEntitlementID), - zap.String("dst_entitlement_id", dstEntitlementID), - zap.Bool("shallow", shallow), - zap.Strings("resource_type_ids", resourceTypeIDs), - ) - return nil - } - return nil -} - -func (g *EntitlementGraph) removeNode(nodeID int) { - delete(g.Nodes, nodeID) - delete(g.Edges, nodeID) - for id := range g.Edges { - delete(g.Edges[id], nodeID) - } -} - -func (g *EntitlementGraph) mergeNodes(node1ID int, node2ID int) { - node1 := g.Nodes[node1ID] - node2 := g.Nodes[node2ID] - - // Put node's entitlements on first node - node1.EntitlementIDs = append(node1.EntitlementIDs, node2.EntitlementIDs...) - - // TODO: Merge grant info? - - for dstNodeID := range g.Edges[node2ID] { - dstNode := g.Nodes[dstNodeID] - if dstNodeID == node1ID { - continue - } - - // Set outgoing edges on first node - for id := range g.Edges[dstNodeID] { - if id != node1.Id { - g.Edges[node1.Id][id] = g.Edges[dstNode.Id][id] - } - } - } - - // Set incoming edges on first node - for srcID, edges := range g.Edges { - for dstID, gi := range edges { - if dstID == node2ID && srcID != node1ID { - g.Edges[srcID][node1ID] = gi - delete(g.Edges[srcID], dstID) - } - } - } - - // Delete node - g.removeNode(node2ID) -} - -func (g *EntitlementGraph) FixCycles() error { - // If we can't fix the cycles in 10 tries, just give up - const maxTries = 10 - prevCycleCount := 0 - for i := 0; i < maxTries; i++ { - cycles, hasCycles := g.GetCycles() - if !hasCycles { - return nil - } - cycleCount := len(cycles) - if cycleCount < prevCycleCount { - // Reset the number of tries if we made progress - i = 0 - } - prevCycleCount = cycleCount - - // Merge all the nodes in a cycle. - for i := 1; i < len(cycles[0]); i++ { - g.mergeNodes(cycles[0][0], cycles[0][i]) - } - } - return fmt.Errorf("could not fix cycles after %v tries", maxTries) -} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/cycle.go b/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/cycle.go new file mode 100644 index 00000000..bea170c2 --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/cycle.go @@ -0,0 +1,213 @@ +package expand + +import ( + mapset "github.com/deckarep/golang-set/v2" +) + +// GetFirstCycle given an entitlements graph, return a cycle by node ID if it +// exists. Returns nil if no cycle exists. If there is a single +// node pointing to itself, that will count as a cycle. +func (g *EntitlementGraph) GetFirstCycle() []int { + visited := mapset.NewSet[int]() + for nodeID := range g.Nodes { + cycle, hasCycle := g.cycleDetectionHelper(nodeID, visited, []int{}) + if hasCycle { + return cycle + } + } + + return nil +} + +func (g *EntitlementGraph) cycleDetectionHelper( + nodeID int, + visited mapset.Set[int], + currentCycle []int, +) ([]int, bool) { + visited.Add(nodeID) + if destinations, ok := g.SourcesToDestinations[nodeID]; ok { + for destinationID := range destinations { + nextCycle := make([]int, len(currentCycle)) + copy(nextCycle, currentCycle) + nextCycle = append(nextCycle, nodeID) + + if !visited.Contains(destinationID) { + if cycle, hasCycle := g.cycleDetectionHelper(destinationID, visited, nextCycle); hasCycle { + return cycle, true + } + } else { + // Make sure to not include part of the start before the cycle. + outputCycle := make([]int, 0) + for i := len(nextCycle) - 1; i >= 0; i-- { + outputCycle = append(outputCycle, nextCycle[i]) + if nextCycle[i] == destinationID { + return outputCycle, true + } + } + } + } + } + return nil, false +} + +// removeNode obliterates a node and all incoming/outgoing edges. +func (g *EntitlementGraph) removeNode(nodeID int) { + // Delete from reverse mapping. + if node, ok := g.Nodes[nodeID]; ok { + for _, entitlementID := range node.EntitlementIDs { + entNodeId, ok := g.EntitlementsToNodes[entitlementID] + if ok && entNodeId == nodeID { + delete(g.EntitlementsToNodes, entitlementID) + } + } + } + + // Delete from nodes list. + delete(g.Nodes, nodeID) + + // Delete all outgoing edges. + if destinations, ok := g.SourcesToDestinations[nodeID]; ok { + for destinationID, edgeID := range destinations { + delete(g.DestinationsToSources[destinationID], nodeID) + delete(g.Edges, edgeID) + } + } + delete(g.SourcesToDestinations, nodeID) + + // Delete all incoming edges. + if sources, ok := g.DestinationsToSources[nodeID]; ok { + for sourceID, edgeID := range sources { + delete(g.SourcesToDestinations[sourceID], nodeID) + delete(g.Edges, edgeID) + } + } + delete(g.SourcesToDestinations, nodeID) +} + +// FixCycles if any cycles of nodes exist, merge all nodes in that cycle into a +// single node and then repeat. Iteration ends when there are no more cycles. +func (g *EntitlementGraph) FixCycles() error { + cycle := g.GetFirstCycle() + if cycle == nil { + return nil + } + + if err := g.fixCycle(cycle); err != nil { + return err + } + + // Recurse! + return g.FixCycles() +} + +// fixCycle takes a list of Node IDs that form a cycle and merges them into a +// single, new node. +func (g *EntitlementGraph) fixCycle(nodeIDs []int) error { + entitlementIDs := mapset.NewSet[string]() + outgoingEdgesToResourceTypeIDs := map[int]mapset.Set[string]{} + incomingEdgesToResourceTypeIDs := map[int]mapset.Set[string]{} + for _, nodeID := range nodeIDs { + if node, ok := g.Nodes[nodeID]; ok { + // Gather entitlements. + for _, entitlementID := range node.EntitlementIDs { + entitlementIDs.Add(entitlementID) + } + + // Gather all incoming edges. + if sources, ok := g.DestinationsToSources[nodeID]; ok { + for sourceNodeID, edgeID := range sources { + if edge, ok := g.Edges[edgeID]; ok { + resourceTypeIDs, ok := incomingEdgesToResourceTypeIDs[sourceNodeID] + if !ok { + resourceTypeIDs = mapset.NewSet[string]() + } + for _, resourceTypeID := range edge.ResourceTypeIDs { + resourceTypeIDs.Add(resourceTypeID) + } + incomingEdgesToResourceTypeIDs[sourceNodeID] = resourceTypeIDs + } + } + } + + // Gather all outgoing edges. + if destinations, ok := g.SourcesToDestinations[nodeID]; ok { + for destinationNodeID, edgeID := range destinations { + if edge, ok := g.Edges[edgeID]; ok { + resourceTypeIDs, ok := outgoingEdgesToResourceTypeIDs[destinationNodeID] + if !ok { + resourceTypeIDs = mapset.NewSet[string]() + } + for _, resourceTypeID := range edge.ResourceTypeIDs { + resourceTypeIDs.Add(resourceTypeID) + } + outgoingEdgesToResourceTypeIDs[destinationNodeID] = resourceTypeIDs + } + } + } + } + } + + // Create a new node with the entitlements. + g.NextNodeID++ + node := Node{ + Id: g.NextNodeID, + EntitlementIDs: entitlementIDs.ToSlice(), + } + g.Nodes[node.Id] = node + for entitlementID := range entitlementIDs.Iter() { + // Break the old connections and point to this node. + g.EntitlementsToNodes[entitlementID] = node.Id + } + + // Hook up edges + for destinationID, resourceTypeIDs := range outgoingEdgesToResourceTypeIDs { + g.NextEdgeID++ + edge := Edge{ + EdgeID: g.NextEdgeID, + SourceID: node.Id, + DestinationID: destinationID, + IsExpanded: false, + IsShallow: false, + ResourceTypeIDs: resourceTypeIDs.ToSlice(), + } + g.Edges[edge.EdgeID] = edge + if _, ok := g.SourcesToDestinations[node.Id]; !ok { + g.SourcesToDestinations[node.Id] = make(map[int]int) + } + g.SourcesToDestinations[node.Id][destinationID] = edge.EdgeID + if _, ok := g.DestinationsToSources[destinationID]; !ok { + g.DestinationsToSources[destinationID] = make(map[int]int) + } + g.DestinationsToSources[destinationID][node.Id] = edge.EdgeID + } + for sourceID, resourceTypeIDs := range incomingEdgesToResourceTypeIDs { + g.NextEdgeID++ + edge := Edge{ + EdgeID: g.NextEdgeID, + SourceID: sourceID, + DestinationID: node.Id, + IsExpanded: false, + IsShallow: false, + ResourceTypeIDs: resourceTypeIDs.ToSlice(), + } + g.Edges[edge.EdgeID] = edge + + if _, ok := g.SourcesToDestinations[sourceID]; !ok { + g.SourcesToDestinations[sourceID] = make(map[int]int) + } + g.SourcesToDestinations[sourceID][node.Id] = edge.EdgeID + + if _, ok := g.DestinationsToSources[node.Id]; !ok { + g.DestinationsToSources[node.Id] = make(map[int]int) + } + g.DestinationsToSources[node.Id][sourceID] = edge.EdgeID + } + + // Call delete to delete the node and every associated edge. This will + // conveniently delete all edges that were internal to the cycle. + for _, nodeID := range nodeIDs { + g.removeNode(nodeID) + } + + return nil +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/graph.go b/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/graph.go new file mode 100644 index 00000000..dbdce819 --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/graph.go @@ -0,0 +1,283 @@ +package expand + +import ( + "context" + + v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "go.uber.org/zap" +) + +type EntitlementGraphAction struct { + SourceEntitlementID string `json:"source_entitlement_id"` + DescendantEntitlementID string `json:"descendant_entitlement_id"` + Shallow bool `json:"shallow"` + ResourceTypeIDs []string `json:"resource_types_ids"` + PageToken string `json:"page_token"` +} + +type Edge struct { + EdgeID int `json:"edge_id"` + SourceID int `json:"source_id"` + DestinationID int `json:"destination_id"` + IsExpanded bool `json:"expanded"` + IsShallow bool `json:"shallow"` + ResourceTypeIDs []string `json:"resource_type_ids"` +} + +// Node represents a list of entitlements. It is the base element of the graph. +type Node struct { + Id int `json:"id"` + EntitlementIDs []string `json:"entitlementIds"` // List of entitlements. +} + +// EntitlementGraph - a directed graph representing the relationships between +// entitlements and grants. This data structure is naïve to any business logic. +// Note that the data of each Node is actually a list or IDs, not a single ID. +// This is because the graph can have cycles, and we address them by reducing +// _all_ nodes in a cycle into a single node. +type EntitlementGraph struct { + NextNodeID int `json:"next_node_id"` // Automatically incremented so that each node has a unique ID. + NextEdgeID int `json:"next_edge_id"` // Automatically incremented so that each edge has a unique ID. + Nodes map[int]Node `json:"nodes"` // The mapping of all node IDs to nodes. + EntitlementsToNodes map[string]int `json:"entitlements_to_nodes"` // Internal mapping of entitlements to nodes for quicker lookup. + SourcesToDestinations map[int]map[int]int `json:"sources_to_destinations"` // Internal mapping of outgoing edges by node ID. + DestinationsToSources map[int]map[int]int `json:"destinations_to_sources"` // Internal mapping of incoming edges by node ID. + Edges map[int]Edge `json:"edges"` // Adjacency list. Source node -> descendant node + Loaded bool `json:"loaded"` + Depth int `json:"depth"` + Actions []EntitlementGraphAction `json:"actions"` +} + +func NewEntitlementGraph(_ context.Context) *EntitlementGraph { + return &EntitlementGraph{ + DestinationsToSources: make(map[int]map[int]int), + Edges: make(map[int]Edge), + EntitlementsToNodes: make(map[string]int), + Nodes: make(map[int]Node), + SourcesToDestinations: make(map[int]map[int]int), + } +} + +// isNodeExpanded - is every outgoing edge from this node Expanded? +func (g *EntitlementGraph) isNodeExpanded(nodeID int) bool { + for _, edgeID := range g.SourcesToDestinations[nodeID] { + if edge, ok := g.Edges[edgeID]; ok { + if !edge.IsExpanded { + return false + } + } + } + return true +} + +// IsExpanded returns true if all entitlements in the graph have been expanded. +func (g *EntitlementGraph) IsExpanded() bool { + for _, edge := range g.Edges { + if !edge.IsExpanded { + return false + } + } + return true +} + +// IsEntitlementExpanded returns true if all the outgoing edges for the given entitlement have been expanded. +func (g *EntitlementGraph) IsEntitlementExpanded(entitlementID string) bool { + node := g.GetNode(entitlementID) + if node == nil { + // TODO: log error? return error? + return false + } + return g.isNodeExpanded(node.Id) +} + +// HasUnexpandedAncestors returns true if the given entitlement has ancestors that have not been expanded yet. +func (g *EntitlementGraph) HasUnexpandedAncestors(entitlementID string) bool { + node := g.GetNode(entitlementID) + if node == nil { + return false + } + + for _, ancestorId := range g.getParents(entitlementID) { + if !g.isNodeExpanded(ancestorId) { + return true + } + } + return false +} + +// getParents gets _all_ IDs of nodes that have this entitlement as a child. +func (g *EntitlementGraph) getParents(entitlementID string) []int { + node := g.GetNode(entitlementID) + if node == nil { + panic("entitlement not found") + } + + parents := make([]int, 0) + if destinations, ok := g.DestinationsToSources[node.Id]; ok { + for id := range destinations { + parents = append(parents, id) + } + } + return parents +} + +// GetDescendantEntitlements given an entitlementID, return a mapping of child +// entitlementIDs to edge data. +func (g *EntitlementGraph) GetDescendantEntitlements(entitlementID string) map[string]*Edge { + node := g.GetNode(entitlementID) + if node == nil { + return nil + } + entitlementsToEdges := make(map[string]*Edge) + if destinations, ok := g.SourcesToDestinations[node.Id]; ok { + for destinationID, edgeID := range destinations { + if destination, ok := g.Nodes[destinationID]; ok { + for _, entitlementID := range destination.EntitlementIDs { + if edge, ok := g.Edges[edgeID]; ok { + entitlementsToEdges[entitlementID] = &edge + } + } + } + } + } + return entitlementsToEdges +} + +func (g *EntitlementGraph) HasEntitlement(entitlementID string) bool { + return g.GetNode(entitlementID) != nil +} + +// AddEntitlement - add an entitlement's ID as an unconnected node in the graph. +func (g *EntitlementGraph) AddEntitlement(entitlement *v2.Entitlement) { + // If the entitlement is already in the graph, fail silently. + found := g.GetNode(entitlement.Id) + if found != nil { + return + } + + // Start at 1 in case we don't initialize something and try to get node 0. + g.NextNodeID++ + + // Create a new node. + node := Node{ + Id: g.NextNodeID, + EntitlementIDs: []string{entitlement.Id}, + } + + // Add the node to the data structures. + g.Nodes[node.Id] = node + g.EntitlementsToNodes[entitlement.Id] = node.Id +} + +// GetEntitlements returns a combined list of _all_ entitlements from all nodes. +func (g *EntitlementGraph) GetEntitlements() []string { + var entitlements []string + for _, node := range g.Nodes { + entitlements = append(entitlements, node.EntitlementIDs...) + } + return entitlements +} + +// MarkEdgeExpanded given source and destination entitlements, mark the edge +// between them as "expanded". +func (g *EntitlementGraph) MarkEdgeExpanded(sourceEntitlementID string, descendantEntitlementID string) { + srcNode := g.GetNode(sourceEntitlementID) + if srcNode == nil { + // TODO: panic? + return + } + dstNode := g.GetNode(descendantEntitlementID) + if dstNode == nil { + // TODO: panic? + return + } + + if destinations, ok := g.SourcesToDestinations[srcNode.Id]; ok { + if edgeID, ok := destinations[dstNode.Id]; ok { + if edge, ok := g.Edges[edgeID]; ok { + edge.IsExpanded = true + g.Edges[edgeID] = edge + } + } + } +} + +// GetNode - returns the node that contains the given `entitlementID`. +func (g *EntitlementGraph) GetNode(entitlementID string) *Node { + nodeID, ok := g.EntitlementsToNodes[entitlementID] + if !ok { + return nil + } + node, ok := g.Nodes[nodeID] + if !ok { + return nil + } + return &node +} + +// AddEdge - given two entitlements, add an edge with resourceTypeIDs. +func (g *EntitlementGraph) AddEdge( + ctx context.Context, + srcEntitlementID string, + dstEntitlementID string, + isShallow bool, + resourceTypeIDs []string, +) error { + srcNode := g.GetNode(srcEntitlementID) + if srcNode == nil { + return ErrNoEntitlement + } + dstNode := g.GetNode(dstEntitlementID) + if dstNode == nil { + return ErrNoEntitlement + } + + if destinations, ok := g.SourcesToDestinations[srcNode.Id]; ok { + if _, ok = destinations[dstNode.Id]; ok { + // TODO: just do nothing? it's probably a mistake if we're adding the same edge twice + ctxzap.Extract(ctx).Warn( + "duplicate edge from datasource", + zap.String("src_entitlement_id", srcEntitlementID), + zap.String("dst_entitlement_id", dstEntitlementID), + zap.Bool("shallow", isShallow), + zap.Strings("resource_type_ids", resourceTypeIDs), + ) + return nil + } + } else { + g.SourcesToDestinations[srcNode.Id] = make(map[int]int) + } + + if sources, ok := g.DestinationsToSources[dstNode.Id]; ok { + if _, ok = sources[srcNode.Id]; ok { + // TODO: just do nothing? it's probably a mistake if we're adding the same edge twice + ctxzap.Extract(ctx).Warn( + "duplicate edge from datasource", + zap.String("src_entitlement_id", srcEntitlementID), + zap.String("dst_entitlement_id", dstEntitlementID), + zap.Bool("shallow", isShallow), + zap.Strings("resource_type_ids", resourceTypeIDs), + ) + return nil + } + } else { + g.DestinationsToSources[dstNode.Id] = make(map[int]int) + } + + // Start at 1 in case we don't initialize something and try to get edge 0. + g.NextEdgeID++ + edge := Edge{ + EdgeID: g.NextEdgeID, + SourceID: srcNode.Id, + DestinationID: dstNode.Id, + IsExpanded: false, + IsShallow: isShallow, + ResourceTypeIDs: resourceTypeIDs, + } + + g.Edges[g.NextEdgeID] = edge + g.SourcesToDestinations[srcNode.Id][dstNode.Id] = edge.EdgeID + g.DestinationsToSources[dstNode.Id][srcNode.Id] = edge.EdgeID + return nil +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/validate.go b/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/validate.go new file mode 100644 index 00000000..fdb94e87 --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/validate.go @@ -0,0 +1,134 @@ +package expand + +import ( + "errors" + "fmt" + "slices" + "strings" +) + +var ( + ErrNoEntitlement = errors.New("no entitlement found") +) + +func (node *Node) Str() string { + return fmt.Sprintf( + "node %v entitlement IDs: %v", + node.Id, + node.EntitlementIDs, + ) +} + +func (edge *Edge) Str() string { + return fmt.Sprintf( + "%v -> %v { expanded: %v, shallow: %v, resources: %v }", + edge.SourceID, + edge.DestinationID, + edge.IsExpanded, + edge.IsShallow, + edge.ResourceTypeIDs, + ) +} + +// Str lists every `node` line by line followed by every `edge`. Useful for debugging. +func (g *EntitlementGraph) Str() string { + nodeHeader := "" + edgeHeader := "edges:" + nodesStrings := make([]string, 0, len(g.Nodes)) + edgeStrings := make([]string, 0, len(g.Edges)) + + for id, node := range g.Nodes { + nodesStrings = append( + nodesStrings, + node.Str(), + ) + if destinationsMap, destinationOK := g.SourcesToDestinations[id]; destinationOK { + for _, edgeID := range destinationsMap { + if edge, edgeOK := g.Edges[edgeID]; edgeOK { + edgeStrings = append( + edgeStrings, + edge.Str(), + ) + } + } + } + } + + return strings.Join( + []string{ + nodeHeader, + strings.Join(nodesStrings, "\n"), + edgeHeader, + strings.Join(edgeStrings, "\n"), + }, + "\n", + ) +} + +// validateEdges validates that for every edge, both nodes actually exists. +func (g *EntitlementGraph) validateEdges() error { + for edgeId, edge := range g.Edges { + if _, ok := g.Nodes[edge.SourceID]; !ok { + return ErrNoEntitlement + } + if _, ok := g.Nodes[edge.DestinationID]; !ok { + return ErrNoEntitlement + } + + if g.SourcesToDestinations[edge.SourceID][edge.DestinationID] != edgeId { + return fmt.Errorf("edge %v does not match source %v to destination %v", edgeId, edge.SourceID, edge.DestinationID) + } + + if g.DestinationsToSources[edge.DestinationID][edge.SourceID] != edgeId { + return fmt.Errorf("edge %v does not match destination %v to source %v", edgeId, edge.DestinationID, edge.SourceID) + } + } + return nil +} + +// validateNodes validates that each node has at least one `entitlementID` and +// that each `entitlementID` only appears once in the graph. +func (g *EntitlementGraph) validateNodes() error { + // check for entitlement ids that are in multiple nodes + seenEntitlements := make(map[string]int) + for nodeID, node := range g.Nodes { + if len(node.EntitlementIDs) == 0 { + return fmt.Errorf("empty node %v", nodeID) + } + for _, entID := range node.EntitlementIDs { + if _, ok := seenEntitlements[entID]; ok { + return fmt.Errorf("entitlement %v is in multiple nodes: %v %v", entID, nodeID, seenEntitlements[entID]) + } + seenEntitlements[entID] = nodeID + entNodeId, ok := g.EntitlementsToNodes[entID] + if !ok { + return fmt.Errorf("entitlement %v is not in EntitlementsToNodes. should be in node %v", entID, nodeID) + } + if entNodeId != nodeID { + return fmt.Errorf("entitlement %v is in node %v but should be in node %v", entID, entNodeId, nodeID) + } + } + } + + for entID, nodeID := range g.EntitlementsToNodes { + node, ok := g.Nodes[nodeID] + if !ok { + return fmt.Errorf("entitlement %v is in EntitlementsToNodes but not in Nodes", entID) + } + if !slices.Contains(node.EntitlementIDs, entID) { + return fmt.Errorf("entitlement %v is in EntitlementsToNodes but not in node %v", entID, nodeID) + } + } + return nil +} + +// Validate checks every node and edge and returns an error if the graph is not valid. +func (g *EntitlementGraph) Validate() error { + if err := g.validateEdges(); err != nil { + return err + } + if err := g.validateNodes(); err != nil { + return err + } + return nil +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/sync/state.go b/vendor/github.com/conductorone/baton-sdk/pkg/sync/state.go index 60674f18..df651a4a 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/sync/state.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/sync/state.go @@ -6,6 +6,8 @@ import ( "fmt" "sync" + "github.com/conductorone/baton-sdk/pkg/sync/expand" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "go.uber.org/zap" ) @@ -16,7 +18,7 @@ type State interface { NextPage(ctx context.Context, pageToken string) error ResourceTypeID(ctx context.Context) string ResourceID(ctx context.Context) string - EntitlementGraph(ctx context.Context) *EntitlementGraph + EntitlementGraph(ctx context.Context) *expand.EntitlementGraph ParentResourceID(ctx context.Context) string ParentResourceTypeID(ctx context.Context) string PageToken(ctx context.Context) string @@ -52,12 +54,12 @@ func (s ActionOp) String() string { } } -// MarshalJSON marshals the ActionOp insto a json string. +// MarshalJSON marshals the ActionOp into a json string. func (s *ActionOp) MarshalJSON() ([]byte, error) { return json.Marshal(s.String()) } -// UnmarshalJSON unmarshal's the input byte slice and updates this action op. +// UnmarshalJSON unmarshals the input byte slice and updates this action op. func (s *ActionOp) UnmarshalJSON(data []byte) error { var v string err := json.Unmarshal(data, &v) @@ -118,21 +120,21 @@ type state struct { mtx sync.RWMutex actions []Action currentAction *Action - entitlementGraph *EntitlementGraph + entitlementGraph *expand.EntitlementGraph needsExpansion bool } // serializedToken is used to serialize the token to JSON. This separate object is used to avoid having exported fields // on the object used externally. We should interface this, probably. type serializedToken struct { - Actions []Action `json:"actions"` - CurrentAction *Action `json:"current_action"` - NeedsExpansion bool `json:"needs_expansion"` - EntitlementGraph *EntitlementGraph `json:"entitlement_graph"` + Actions []Action `json:"actions"` + CurrentAction *Action `json:"current_action"` + NeedsExpansion bool `json:"needs_expansion"` + EntitlementGraph *expand.EntitlementGraph `json:"entitlement_graph"` } // push adds a new action to the stack. If there is no current state, the action is directly set to current, else -// the current state is appened to the slice of actions, and the new action is set to current. +// the current state is appended to the slice of actions, and the new action is set to current. func (st *state) push(action Action) { st.mtx.Lock() defer st.mtx.Unlock() @@ -291,13 +293,13 @@ func (st *state) ResourceID(ctx context.Context) string { } // EntitlementGraph returns the entitlement graph for the current action. -func (st *state) EntitlementGraph(ctx context.Context) *EntitlementGraph { +func (st *state) EntitlementGraph(ctx context.Context) *expand.EntitlementGraph { c := st.Current() if c == nil { panic("no current state") } if st.entitlementGraph == nil { - st.entitlementGraph = NewEntitlementGraph(ctx) + st.entitlementGraph = expand.NewEntitlementGraph(ctx) } return st.entitlementGraph } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/sync/syncer.go b/vendor/github.com/conductorone/baton-sdk/pkg/sync/syncer.go index fa0958b8..46e67c84 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/sync/syncer.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/sync/syncer.go @@ -11,6 +11,8 @@ import ( "strconv" "time" + "github.com/conductorone/baton-sdk/pkg/sync/expand" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "go.uber.org/zap" "google.golang.org/grpc/codes" @@ -50,6 +52,7 @@ type syncer struct { transitionHandler func(s Action) progressHandler func(p *Progress) tmpDir string + skipFullSync bool skipEGForResourceType map[string]bool } @@ -80,18 +83,27 @@ func (s *syncer) handleProgress(ctx context.Context, a *Action, c int) { } } +var attempts = 0 + func shouldWaitAndRetry(ctx context.Context, err error) bool { + if err == nil { + attempts = 0 + return true + } if status.Code(err) != codes.Unavailable { return false } + attempts++ l := ctxzap.Extract(ctx) - l.Error("retrying operation", zap.Error(err)) + + var wait time.Duration = time.Duration(attempts) * time.Second + + l.Error("retrying operation", zap.Error(err), zap.Duration("wait", wait)) for { select { - // TODO: this should back off based on error counts - case <-time.After(1 * time.Second): + case <-time.After(wait): return true case <-ctx.Done(): return false @@ -101,9 +113,13 @@ func shouldWaitAndRetry(ctx context.Context, err error) bool { // Sync starts the syncing process. The sync process is driven by the action stack that is part of the state object. // For each page of data that is required to be fetched from the connector, a new action is pushed on to the stack. Once -// an action is completed, it is popped off of the queue. Before procesing each action, we checkpoint the state object -// into the datasouce. This allows for graceful resumes if a sync is interrupted. +// an action is completed, it is popped off of the queue. Before processing each action, we checkpoint the state object +// into the datasource. This allows for graceful resumes if a sync is interrupted. func (s *syncer) Sync(ctx context.Context) error { + if s.skipFullSync { + return s.SkipSync(ctx) + } + l := ctxzap.Extract(ctx) runCtx := ctx @@ -189,28 +205,28 @@ func (s *syncer) Sync(ctx context.Context) error { case SyncResourceTypesOp: err = s.SyncResourceTypes(ctx) - if err != nil && !shouldWaitAndRetry(ctx, err) { + if !shouldWaitAndRetry(ctx, err) { return err } continue case SyncResourcesOp: err = s.SyncResources(ctx) - if err != nil && !shouldWaitAndRetry(ctx, err) { + if !shouldWaitAndRetry(ctx, err) { return err } continue case SyncEntitlementsOp: err = s.SyncEntitlements(ctx) - if err != nil && !shouldWaitAndRetry(ctx, err) { + if !shouldWaitAndRetry(ctx, err) { return err } continue case SyncGrantsOp: err = s.SyncGrants(ctx) - if err != nil && !shouldWaitAndRetry(ctx, err) { + if !shouldWaitAndRetry(ctx, err) { return err } continue @@ -230,7 +246,7 @@ func (s *syncer) Sync(ctx context.Context) error { } err = s.SyncGrantExpansion(ctx) - if err != nil && !shouldWaitAndRetry(ctx, err) { + if !shouldWaitAndRetry(ctx, err) { return err } continue @@ -254,6 +270,46 @@ func (s *syncer) Sync(ctx context.Context) error { return nil } +func (s *syncer) SkipSync(ctx context.Context) error { + l := ctxzap.Extract(ctx) + l.Info("skipping sync") + + var runCanc context.CancelFunc + if s.runDuration > 0 { + _, runCanc = context.WithTimeout(ctx, s.runDuration) + } + if runCanc != nil { + defer runCanc() + } + + err := s.loadStore(ctx) + if err != nil { + return err + } + + _, err = s.connector.Validate(ctx, &v2.ConnectorServiceValidateRequest{}) + if err != nil { + return err + } + + _, err = s.store.StartNewSync(ctx) + if err != nil { + return err + } + + err = s.store.EndSync(ctx) + if err != nil { + return err + } + + err = s.store.Cleanup(ctx) + if err != nil { + return err + } + + return nil +} + // SyncResourceTypes calls the ListResourceType() connector endpoint and persists the results in to the datasource. func (s *syncer) SyncResourceTypes(ctx context.Context) error { pageToken := s.state.PageToken(ctx) @@ -273,11 +329,9 @@ func (s *syncer) SyncResourceTypes(ctx context.Context) error { return err } - for _, rt := range resp.List { - err = s.store.PutResourceType(ctx, rt) - if err != nil { - return err - } + err = s.store.PutResourceTypes(ctx, resp.List...) + if err != nil { + return err } s.handleProgress(ctx, s.state.Current(), len(resp.List)) @@ -382,6 +436,7 @@ func (s *syncer) syncResources(ctx context.Context) error { } } + bulkPutResoruces := []*v2.Resource{} for _, r := range resp.List { // Check if we've already synced this resource, skip it if we have _, err = s.store.GetResource(ctx, &reader_v2.ResourcesReaderServiceGetResourceRequest{ @@ -403,12 +458,16 @@ func (s *syncer) syncResources(ctx context.Context) error { // Set the resource creation source r.CreationSource = v2.Resource_CREATION_SOURCE_CONNECTOR_LIST_RESOURCES - err = s.store.PutResource(ctx, r) + bulkPutResoruces = append(bulkPutResoruces, r) + + err = s.getSubResources(ctx, r) if err != nil { return err } + } - err = s.getSubResources(ctx, r) + if len(bulkPutResoruces) > 0 { + err = s.store.PutResources(ctx, bulkPutResoruces...) if err != nil { return err } @@ -480,7 +539,7 @@ func (s *syncer) shouldSkipEntitlementsAndGrants(ctx context.Context, r *v2.Reso } // SyncEntitlements fetches the entitlements from the connector. It first lists each resource from the datastore, -// and pushes an action to fetch the entitelments for each resource. +// and pushes an action to fetch the entitlements for each resource. func (s *syncer) SyncEntitlements(ctx context.Context) error { if s.state.ResourceTypeID(ctx) == "" && s.state.ResourceID(ctx) == "" { pageToken := s.state.PageToken(ctx) @@ -548,11 +607,9 @@ func (s *syncer) syncEntitlementsForResource(ctx context.Context, resourceID *v2 if err != nil { return err } - for _, e := range resp.List { - err = s.store.PutEntitlement(ctx, e) - if err != nil { - return err - } + err = s.store.PutEntitlements(ctx, resp.List...) + if err != nil { + return err } s.handleProgress(ctx, s.state.Current(), len(resp.List)) @@ -810,9 +867,13 @@ func (s *syncer) SyncGrantExpansion(ctx context.Context) error { } if entitlementGraph.Loaded { - cycles, hasCycles := entitlementGraph.GetCycles() - if hasCycles { - l.Warn("cycles detected in entitlement graph", zap.Any("cycles", cycles)) + cycle := entitlementGraph.GetFirstCycle() + if cycle != nil { + l.Warn( + "cycle detected in entitlement graph", + zap.Any("cycle", cycle), + zap.Any("initial graph", entitlementGraph), + ) if dontFixCycles { return fmt.Errorf("cycles detected in entitlement graph") } @@ -883,7 +944,7 @@ func (s *syncer) SyncGrants(ctx context.Context) error { return nil } -type lastestSyncFetcher interface { +type latestSyncFetcher interface { LatestFinishedSync(ctx context.Context) (string, error) } @@ -893,7 +954,7 @@ func (s *syncer) fetchResourceForPreviousSync(ctx context.Context, resourceID *v var previousSyncID string var err error - if psf, ok := s.store.(lastestSyncFetcher); ok { + if psf, ok := s.store.(latestSyncFetcher); ok { previousSyncID, err = psf.LatestFinishedSync(ctx) if err != nil { return "", nil, err @@ -1050,11 +1111,10 @@ func (s *syncer) syncGrantsForResource(ctx context.Context, resourceID *v2.Resou if grantAnnos.Contains(&v2.GrantExpandable{}) { s.state.SetNeedsExpansion() } - - err = s.store.PutGrant(ctx, grant) - if err != nil { - return err - } + } + err = s.store.PutGrants(ctx, grants...) + if err != nil { + return err } s.handleProgress(ctx, s.state.Current(), len(grants)) @@ -1080,7 +1140,7 @@ func (s *syncer) syncGrantsForResource(ctx context.Context, resourceID *v2.Resou if updatedETag != nil { resourceAnnos.Update(updatedETag) resource.Annotations = resourceAnnos - err = s.store.PutResource(ctx, resource) + err = s.store.PutResources(ctx, resource) if err != nil { return err } @@ -1246,7 +1306,7 @@ func (s *syncer) runGrantExpandActions(ctx context.Context) (bool, error) { zap.Any("sources", sources), ) - err = s.store.PutGrant(ctx, descendantGrant) + err = s.store.PutGrants(ctx, descendantGrant) if err != nil { l.Error("runGrantExpandActions: error updating descendant grant", zap.Error(err)) return false, fmt.Errorf("runGrantExpandActions: error updating descendant grant: %w", err) @@ -1300,13 +1360,17 @@ func (s *syncer) expandGrantsForEntitlements(ctx context.Context) error { } if graph.Depth > maxDepth { - l.Error("expandGrantsForEntitlements: exceeded max depth", zap.Any("graph", graph), zap.Int("max_depth", maxDepth)) + l.Error( + "expandGrantsForEntitlements: exceeded max depth", + zap.Any("graph", graph), + zap.Int("max_depth", maxDepth), + ) s.state.FinishAction(ctx) return fmt.Errorf("exceeded max depth") } - // TOOD(morgabra) Yield here after some amount of work? - // traverse edges or call some sort of getentitlements + // TODO(morgabra) Yield here after some amount of work? + // traverse edges or call some sort of getEntitlements for _, sourceEntitlementID := range graph.GetEntitlements() { // We've already expanded this entitlement, so skip it. if graph.IsEntitlementExpanded(sourceEntitlementID) { @@ -1320,14 +1384,14 @@ func (s *syncer) expandGrantsForEntitlements(ctx context.Context) error { } for descendantEntitlementID, grantInfo := range graph.GetDescendantEntitlements(sourceEntitlementID) { - if grantInfo.Expanded { + if grantInfo.IsExpanded { continue } - graph.Actions = append(graph.Actions, EntitlementGraphAction{ + graph.Actions = append(graph.Actions, expand.EntitlementGraphAction{ SourceEntitlementID: sourceEntitlementID, DescendantEntitlementID: descendantEntitlementID, PageToken: "", - Shallow: grantInfo.Shallow, + Shallow: grantInfo.IsShallow, ResourceTypeIDs: grantInfo.ResourceTypeIDs, }) } @@ -1437,6 +1501,12 @@ func WithTmpDir(path string) SyncOpt { } } +func WithSkipFullSync() SyncOpt { + return func(s *syncer) { + s.skipFullSync = true + } +} + // NewSyncer returns a new syncer object. func NewSyncer(ctx context.Context, c types.ConnectorClient, opts ...SyncOpt) (Syncer, error) { s := &syncer{ diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/create_ticket.go b/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/create_ticket.go index e5a15bfd..b4af9525 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/create_ticket.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/create_ticket.go @@ -30,7 +30,7 @@ func (c *createTicketTaskHandler) HandleTask(ctx context.Context) error { t := c.task.GetCreateTicketTask() if t == nil || t.GetTicketRequest() == nil { l.Error("create ticket task was nil or missing ticket request", zap.Any("create_ticket_task", t)) - return c.helpers.FinishTask(ctx, nil, nil, errors.Join(errors.New("malformed create ticket task"), ErrTaskNonRetryable)) + return c.helpers.FinishTask(ctx, nil, t.GetAnnotations(), errors.Join(errors.New("malformed create ticket task"), ErrTaskNonRetryable)) } cc := c.helpers.ConnectorClient() @@ -41,7 +41,7 @@ func (c *createTicketTaskHandler) HandleTask(ctx context.Context) error { }) if err != nil { l.Error("failed creating ticket", zap.Error(err)) - return c.helpers.FinishTask(ctx, nil, nil, errors.Join(err, ErrTaskNonRetryable)) + return c.helpers.FinishTask(ctx, nil, t.GetAnnotations(), err) } respAnnos := annotations.Annotations(resp.GetAnnotations()) diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/full_sync.go b/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/full_sync.go index 8a72c047..e5dd447e 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/full_sync.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/full_sync.go @@ -6,15 +6,14 @@ import ( "io" "os" - "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" - "go.uber.org/zap" - "google.golang.org/protobuf/proto" - v1 "github.com/conductorone/baton-sdk/pb/c1/connectorapi/baton/v1" "github.com/conductorone/baton-sdk/pkg/annotations" sdkSync "github.com/conductorone/baton-sdk/pkg/sync" "github.com/conductorone/baton-sdk/pkg/tasks" "github.com/conductorone/baton-sdk/pkg/types" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" ) type fullSyncHelpers interface { @@ -26,13 +25,24 @@ type fullSyncHelpers interface { } type fullSyncTaskHandler struct { - task *v1.Task - helpers fullSyncHelpers + task *v1.Task + helpers fullSyncHelpers + skipFullSync bool } func (c *fullSyncTaskHandler) sync(ctx context.Context, c1zPath string) error { l := ctxzap.Extract(ctx).With(zap.String("task_id", c.task.GetId()), zap.Stringer("task_type", tasks.GetType(c.task))) - syncer, err := sdkSync.NewSyncer(ctx, c.helpers.ConnectorClient(), sdkSync.WithC1ZPath(c1zPath), sdkSync.WithTmpDir(c.helpers.TempDir())) + + syncOpts := []sdkSync.SyncOpt{ + sdkSync.WithC1ZPath(c1zPath), + sdkSync.WithTmpDir(c.helpers.TempDir()), + } + + if c.skipFullSync { + syncOpts = append(syncOpts, sdkSync.WithSkipFullSync()) + } + + syncer, err := sdkSync.NewSyncer(ctx, c.helpers.ConnectorClient(), syncOpts...) if err != nil { l.Error("failed to create syncer", zap.Error(err)) return err @@ -71,7 +81,6 @@ func (c *fullSyncTaskHandler) sync(ctx context.Context, c1zPath string) error { func (c *fullSyncTaskHandler) HandleTask(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() - l := ctxzap.Extract(ctx).With(zap.String("task_id", c.task.GetId()), zap.Stringer("task_type", tasks.GetType(c.task))) l.Info("Handling full sync task.") @@ -124,9 +133,10 @@ func (c *fullSyncTaskHandler) HandleTask(ctx context.Context) error { return c.helpers.FinishTask(ctx, nil, nil, nil) } -func newFullSyncTaskHandler(task *v1.Task, helpers fullSyncHelpers) tasks.TaskHandler { +func newFullSyncTaskHandler(task *v1.Task, helpers fullSyncHelpers, skipFullSync bool) tasks.TaskHandler { return &fullSyncTaskHandler{ - task: task, - helpers: helpers, + task: task, + helpers: helpers, + skipFullSync: skipFullSync, } } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/get_ticket.go b/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/get_ticket.go index 70a13913..18ce23e8 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/get_ticket.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/get_ticket.go @@ -3,7 +3,6 @@ package c1api import ( "context" "errors" - "fmt" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" v1 "github.com/conductorone/baton-sdk/pb/c1/connectorapi/baton/v1" @@ -39,11 +38,11 @@ func (c *getTicketTaskHandler) HandleTask(ctx context.Context) error { Id: t.GetTicketId(), }) if err != nil { - return err + return c.helpers.FinishTask(ctx, nil, t.GetAnnotations(), err) } if ticket.GetTicket() == nil { - return fmt.Errorf("connector returned empty ticket") + return c.helpers.FinishTask(ctx, nil, t.GetAnnotations(), errors.Join(errors.New("connector returned empty ticket"), ErrTaskNonRetryable)) } resp := &v2.TicketsServiceGetTicketResponse{ diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/list_ticket_schemas.go b/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/list_ticket_schemas.go index a539d1bf..1c39af24 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/list_ticket_schemas.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/list_ticket_schemas.go @@ -15,6 +15,8 @@ import ( "github.com/conductorone/baton-sdk/pkg/types" ) +const maxTicketSchemas = 100 + type listTicketSchemasTaskHelpers interface { ConnectorClient() types.ConnectorClient FinishTask(ctx context.Context, resp proto.Message, annos annotations.Annotations, err error) error @@ -48,9 +50,22 @@ func (c *listTicketSchemasTaskHandler) HandleTask(ctx context.Context) error { ticketSchemas = append(ticketSchemas, schemas.GetList()...) + // Only return first 100 elements + if len(ticketSchemas) >= maxTicketSchemas { + ignoreCount := len(ticketSchemas) - maxTicketSchemas + ticketSchemas = ticketSchemas[:maxTicketSchemas] + hasAdditionalPages := schemas.GetNextPageToken() != "" + + l.Info("list ticket schemas was greater than or equal to max of 100", + zap.Int("ignoredCount", ignoreCount), + zap.Bool("hasAdditionalPages", hasAdditionalPages)) + break + } + if schemas.GetNextPageToken() == "" { break } + pageToken = schemas.GetNextPageToken() } @@ -60,7 +75,7 @@ func (c *listTicketSchemasTaskHandler) HandleTask(ctx context.Context) error { if err != nil { l.Error("failed listing ticket schemas", zap.Error(err)) - return c.helpers.FinishTask(ctx, nil, nil, errors.Join(err, ErrTaskNonRetryable)) + return c.helpers.FinishTask(ctx, nil, nil, err) } resp := &v2.TicketsServiceListTicketSchemasResponse{ diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/manager.go b/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/manager.go index e1f2a9f4..18f09389 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/manager.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/tasks/c1api/manager.go @@ -47,6 +47,7 @@ type c1ApiTaskManager struct { queue []*v1.Task serviceClient BatonServiceClient tempDir string + skipFullSync bool } // getHeartbeatInterval returns an appropriate heartbeat interval. If the interval is 0, it will return the default heartbeat interval. @@ -222,7 +223,7 @@ func (c *c1ApiTaskManager) Process(ctx context.Context, task *v1.Task, cc types. var handler tasks.TaskHandler switch tasks.GetType(task) { case taskTypes.FullSyncType: - handler = newFullSyncTaskHandler(task, tHelpers) + handler = newFullSyncTaskHandler(task, tHelpers, c.skipFullSync) case taskTypes.HelloType: handler = newHelloTaskHandler(task, tHelpers) @@ -263,7 +264,7 @@ func (c *c1ApiTaskManager) Process(ctx context.Context, task *v1.Task, cc types. return nil } -func NewC1TaskManager(ctx context.Context, clientID string, clientSecret string, tempDir string) (tasks.Manager, error) { +func NewC1TaskManager(ctx context.Context, clientID string, clientSecret string, tempDir string, skipFullSync bool) (tasks.Manager, error) { serviceClient, err := newServiceClient(ctx, clientID, clientSecret) if err != nil { return nil, err @@ -272,5 +273,6 @@ func NewC1TaskManager(ctx context.Context, clientID string, clientSecret string, return &c1ApiTaskManager{ serviceClient: serviceClient, tempDir: tempDir, + skipFullSync: skipFullSync, }, nil } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/tasks/local/ticket.go b/vendor/github.com/conductorone/baton-sdk/pkg/tasks/local/ticket.go index d62dc572..01d93a78 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/tasks/local/ticket.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/tasks/local/ticket.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "github.com/conductorone/baton-sdk/pkg/types/resource" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "go.uber.org/zap" @@ -24,13 +25,14 @@ type localCreateTicket struct { } type ticketTemplate struct { - SchemaID string `json:"schema_id"` - StatusId string `json:"status_id"` - TypeId string `json:"type_id"` - DisplayName string `json:"display_name"` - Description string `json:"description"` - Labels []string `json:"labels"` - CustomFields map[string]interface{} `json:"custom_fields"` + SchemaID string `json:"schema_id"` + StatusId string `json:"status_id"` + TypeId string `json:"type_id"` + DisplayName string `json:"display_name"` + Description string `json:"description"` + Labels []string `json:"labels"` + CustomFields map[string]interface{} `json:"custom_fields"` + RequestedForId string `json:"requested_for_id"` } func (m *localCreateTicket) loadTicketTemplate(ctx context.Context) (*ticketTemplate, error) { @@ -78,10 +80,13 @@ func (m *localCreateTicket) Process(ctx context.Context, task *v1.Task, cc types ticketRequestBody := &v2.TicketRequest{ DisplayName: template.DisplayName, Description: template.Description, - Type: &v2.TicketType{ + Labels: template.Labels, + } + + if template.TypeId != "" { + ticketRequestBody.Type = &v2.TicketType{ Id: template.TypeId, - }, - Labels: template.Labels, + } } if template.StatusId != "" { @@ -90,6 +95,15 @@ func (m *localCreateTicket) Process(ctx context.Context, task *v1.Task, cc types } } + if template.RequestedForId != "" { + rt := resource.NewResourceType("User", []v2.ResourceType_Trait{v2.ResourceType_TRAIT_USER}) + requestedUser, err := resource.NewUserResource(template.RequestedForId, rt, template.RequestedForId, []resource.UserTraitOption{}) + if err != nil { + return err + } + ticketRequestBody.RequestedFor = requestedUser + } + cfs := make(map[string]*v2.TicketCustomField) for k, v := range template.CustomFields { newCfs, err := sdkTicket.CustomFieldForSchemaField(k, schema.Schema, v) diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/types/ticket/custom_fields.go b/vendor/github.com/conductorone/baton-sdk/pkg/types/ticket/custom_fields.go index 332ac763..0d3aaf16 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/types/ticket/custom_fields.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/types/ticket/custom_fields.go @@ -16,6 +16,7 @@ import ( ) var ErrFieldNil = errors.New("error: field is nil") +var ErrTicketValidationError = errors.New("create ticket request is not valid") // CustomFieldForSchemaField returns a typed custom field for a given schema field. func CustomFieldForSchemaField(id string, schema *v2.TicketSchema, value interface{}) (*v2.TicketCustomField, error) { @@ -244,39 +245,36 @@ func GetCustomFieldValue(field *v2.TicketCustomField) (interface{}, error) { func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Ticket) (bool, error) { l := ctxzap.Extract(ctx) - // Look for a matching status - foundMatch := false - for _, status := range schema.GetStatuses() { - // Status is not required - if ticket.Status == nil { - foundMatch = true - break - } - - if ticket.Status.GetId() == status.GetId() { - foundMatch = true - break + // Validate the ticket status is one defined in the schema + // Ticket status is not required so if a ticket doesn't have a status + // we don't need to validate, skip the loop in this case + validTicketStatus := ticket.Status == nil + if !validTicketStatus { + for _, status := range schema.GetStatuses() { + if ticket.Status.GetId() == status.GetId() { + validTicketStatus = true + break + } } } - - if !foundMatch { + if !validTicketStatus { l.Debug("error: invalid ticket: could not find status", zap.String("status_id", ticket.Status.GetId())) return false, nil } - // Look for a matching ticket type - foundMatch = false - for _, tType := range schema.GetTypes() { - if ticket.Type == nil { - return false, nil - } - if ticket.Type.GetId() == tType.GetId() { - foundMatch = true - break + // Validate the ticket type is one defined in the schema + // Ticket type is not required so if a ticket doesn't have a type + // we don't need to validate, skip the loop in this case + validTicketType := ticket.Type == nil + if !validTicketType { + for _, tType := range schema.GetTypes() { + if ticket.Type.GetId() == tType.GetId() { + validTicketType = true + break + } } } - - if !foundMatch { + if !validTicketType { l.Debug("error: invalid ticket: could not find ticket type", zap.String("ticket_type_id", ticket.Type.GetId())) return false, nil } @@ -304,7 +302,7 @@ func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Tic return false, nil } - if cf.Required && tv.StringValue.Value == "" { + if cf.Required && tv.StringValue.GetValue() == "" { l.Debug("error: invalid ticket: string value is required but was empty", zap.String("custom_field_id", cf.Id)) return false, nil } @@ -316,7 +314,7 @@ func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Tic return false, nil } - if cf.Required && len(tv.StringValues.Values) == 0 { + if cf.Required && len(tv.StringValues.GetValues()) == 0 { l.Debug("error: invalid ticket: string values is required but was empty", zap.String("custom_field_id", cf.Id)) return false, nil } @@ -335,7 +333,7 @@ func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Tic return false, nil } - if cf.Required && tv.TimestampValue.Value == nil { + if cf.Required && tv.TimestampValue.GetValue() == nil { l.Debug("error: invalid ticket: expected timestamp value for field but was empty", zap.String("custom_field_id", cf.Id)) return false, nil } @@ -350,7 +348,13 @@ func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Tic ticketValue := tv.PickStringValue.GetValue() allowedValues := v.PickStringValue.GetAllowedValues() - if cf.Required && ticketValue == "" { + // String value is empty but custom field is not required, skip further validation + if !cf.Required && ticketValue == "" { + continue + } + + // Custom field is required, check if string is empty + if ticketValue == "" { l.Debug("error: invalid ticket: expected string value for field but was empty", zap.String("custom_field_id", cf.Id)) return false, nil } @@ -360,7 +364,7 @@ func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Tic return false, nil } - foundMatch = false + foundMatch := false for _, m := range allowedValues { if m == ticketValue { foundMatch = true @@ -387,7 +391,13 @@ func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Tic ticketValues := tv.PickMultipleStringValues.GetValues() allowedValues := v.PickMultipleStringValues.GetAllowedValues() - if cf.Required && len(ticketValues) == 0 { + // String values are empty but custom field is not required, skip further validation + if !cf.Required && len(ticketValues) == 0 { + continue + } + + // Custom field is required so check if string values are empty + if len(ticketValues) == 0 { l.Debug("error: invalid ticket: string values is required but was empty", zap.String("custom_field_id", cf.Id)) return false, nil } @@ -425,7 +435,13 @@ func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Tic ticketValue := tv.PickObjectValue.GetValue() allowedValues := v.PickObjectValue.GetAllowedValues() - if cf.Required && ticketValue == nil || ticketValue.GetId() == "" { + // Object value for field is nil, but custom field is not required, skip further validation + if !cf.Required && (ticketValue == nil || ticketValue.GetId() == "") { + continue + } + + // Custom field is required so check if object value for field is nil + if ticketValue == nil || ticketValue.GetId() == "" { l.Debug("error: invalid ticket: expected object value for field but was nil", zap.String("custom_field_id", cf.Id)) return false, nil } @@ -435,7 +451,7 @@ func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Tic return false, nil } - foundMatch = false + foundMatch := false for _, m := range allowedValues { if m.GetId() == ticketValue.GetId() { foundMatch = true @@ -462,7 +478,13 @@ func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Tic ticketValues := tv.PickMultipleObjectValues.GetValues() allowedValues := v.PickMultipleObjectValues.GetAllowedValues() - if cf.Required && len(ticketValues) == 0 { + // Object values are empty but custom field is not required, skip further validation + if !cf.Required && len(ticketValues) == 0 { + continue + } + + // Custom field is required so check if object values are empty + if len(ticketValues) == 0 { l.Debug("error: invalid ticket: object values is required but was empty", zap.String("custom_field_id", cf.Id)) return false, nil } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/authcredentials.go b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/authcredentials.go index a17624cb..6addfbc7 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/authcredentials.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/authcredentials.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" "golang.org/x/oauth2/jwt" @@ -146,7 +147,7 @@ func (o *OAuth2JWT) GetClient(ctx context.Context, options ...Option) (*http.Cli } func getHttpClient(ctx context.Context, options ...Option) (*http.Client, error) { - options = append(options, WithLogger(true, nil)) + options = append(options, WithLogger(true, ctxzap.Extract(ctx))) httpClient, err := NewClient(ctx, options...) if err != nil { diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/client.go b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/client.go index 7d679515..6b800c58 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/client.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/client.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "net/http" + "time" "go.uber.org/zap" ) @@ -61,11 +62,13 @@ type Option interface { // NewClient creates a new HTTP client that uses the given context and options to create a new transport layer. func NewClient(ctx context.Context, options ...Option) (*http.Client, error) { + httpClient := &http.Client{ + Timeout: 300 * time.Second, // 5 minutes + } t, err := NewTransport(ctx, options...) if err != nil { return nil, err } - return &http.Client{ - Transport: t, - }, nil + httpClient.Transport = t + return httpClient, nil } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/gocache.go b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/gocache.go new file mode 100644 index 00000000..2cbbde41 --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/gocache.go @@ -0,0 +1,170 @@ +package uhttp + +import ( + "bufio" + "bytes" + "context" + "crypto/sha256" + "fmt" + "net/http" + "net/http/httputil" + "sort" + "strings" + "time" + + bigCache "github.com/allegro/bigcache/v3" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "go.uber.org/zap" +) + +type GoCache struct { + rootLibrary *bigCache.BigCache +} + +func NewGoCache(ctx context.Context, cfg CacheConfig) (GoCache, error) { + l := ctxzap.Extract(ctx) + if cfg.DisableCache { + l.Debug("http cache disabled") + return GoCache{}, nil + } + config := bigCache.DefaultConfig(time.Duration(cfg.CacheTTL) * time.Second) + config.Verbose = cfg.LogDebug + config.Shards = 4 + config.HardMaxCacheSize = cfg.CacheMaxSize // value in MB, 0 value means no size limit + cache, err := bigCache.New(ctx, config) + if err != nil { + l.Error("http cache initialization error", zap.Error(err)) + return GoCache{}, err + } + + l.Debug("http cache config", zap.Any("config", config)) + gc := GoCache{ + rootLibrary: cache, + } + + return gc, nil +} + +func (g *GoCache) Statistics() bigCache.Stats { + if g.rootLibrary == nil { + return bigCache.Stats{} + } + + return g.rootLibrary.Stats() +} + +// CreateCacheKey generates a cache key based on the request URL, query parameters, and headers. +// The key is a SHA-256 hash of the normalized URL path, sorted query parameters, and relevant headers. +func CreateCacheKey(req *http.Request) (string, error) { + // Normalize the URL path + path := strings.ToLower(req.URL.Path) + + // Combine the path with sorted query parameters + queryParams := req.URL.Query() + var sortedParams []string + for k, v := range queryParams { + for _, value := range v { + sortedParams = append(sortedParams, fmt.Sprintf("%s=%s", k, value)) + } + } + + sort.Strings(sortedParams) + queryString := strings.Join(sortedParams, "&") + + // Include relevant headers in the cache key + var headerParts []string + for key, values := range req.Header { + for _, value := range values { + if key == "Accept" || key == "Authorization" || key == "Cookie" || key == "Range" { + headerParts = append(headerParts, fmt.Sprintf("%s=%s", key, value)) + } + } + } + + sort.Strings(headerParts) + headersString := strings.Join(headerParts, "&") + + // Create a unique string for the cache key + cacheString := fmt.Sprintf("%s?%s&headers=%s", path, queryString, headersString) + + // Hash the cache string to create a key + hash := sha256.New() + _, err := hash.Write([]byte(cacheString)) + if err != nil { + return "", err + } + + cacheKey := fmt.Sprintf("%x", hash.Sum(nil)) + return cacheKey, nil +} + +func (g *GoCache) Get(key string) (*http.Response, error) { + if g.rootLibrary == nil { + return nil, nil + } + + entry, err := g.rootLibrary.Get(key) + if err == nil { + r := bufio.NewReader(bytes.NewReader(entry)) + resp, err := http.ReadResponse(r, nil) + if err != nil { + return nil, err + } + + return resp, nil + } + + return nil, nil +} + +func (g *GoCache) Set(key string, value *http.Response) error { + if g.rootLibrary == nil { + return nil + } + + cacheableResponse, err := httputil.DumpResponse(value, true) + if err != nil { + return err + } + + err = g.rootLibrary.Set(key, cacheableResponse) + if err != nil { + return err + } + + return nil +} + +func (g *GoCache) Delete(key string) error { + if g.rootLibrary == nil { + return nil + } + + err := g.rootLibrary.Delete(key) + if err != nil { + return err + } + + return nil +} + +func (g *GoCache) Clear() error { + if g.rootLibrary == nil { + return nil + } + + err := g.rootLibrary.Reset() + if err != nil { + return err + } + + return nil +} + +func (g *GoCache) Has(key string) bool { + if g.rootLibrary == nil { + return false + } + _, found := g.rootLibrary.Get(key) + return found == nil +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/transport.go b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/transport.go index a4be4d04..eb08761c 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/transport.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/transport.go @@ -21,6 +21,8 @@ func NewTransport(ctx context.Context, options ...Option) (*Transport, error) { for _, opt := range options { opt.Apply(t) } + t.userAgent = t.userAgent + " baton-sdk/" + sdk.Version + _, err := t.cycle(ctx) if err != nil { return nil, err @@ -104,7 +106,6 @@ func (t *Transport) make(ctx context.Context) (http.RoundTripper, error) { return nil, err } var rv http.RoundTripper = baseTransport - t.userAgent = t.userAgent + " baton-sdk/" + sdk.Version rv = &userAgentTripper{next: rv, userAgent: t.userAgent} return rv, nil } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/wrapper.go b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/wrapper.go index 2b4a1e29..2ae024ba 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/wrapper.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/wrapper.go @@ -5,18 +5,30 @@ import ( "context" "encoding/json" "encoding/xml" + "errors" "fmt" "io" "net/http" "net/url" + "os" + "strconv" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/helpers" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "go.uber.org/zap" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) -const ContentType = "Content-Type" +const ( + ContentType = "Content-Type" + applicationJSON = "application/json" + applicationXML = "application/xml" + applicationFormUrlencoded = "application/x-www-form-urlencoded" + applicationVndApiJSON = "application/vnd.api+json" + acceptHeader = "Accept" +) type WrapperResponse struct { Header http.Header @@ -32,25 +44,88 @@ type ( NewRequest(ctx context.Context, method string, url *url.URL, options ...RequestOption) (*http.Request, error) } BaseHttpClient struct { - HttpClient *http.Client + HttpClient *http.Client + baseHttpCache GoCache } DoOption func(resp *WrapperResponse) error RequestOption func() (io.ReadWriter, map[string]string, error) + ContextKey struct{} + CacheConfig struct { + LogDebug bool + CacheTTL int32 + CacheMaxSize int + DisableCache bool + } ) func NewBaseHttpClient(httpClient *http.Client) *BaseHttpClient { - return &BaseHttpClient{ - HttpClient: httpClient, + ctx := context.TODO() + client, err := NewBaseHttpClientWithContext(ctx, httpClient) + if err != nil { + return nil } + return client } +func NewBaseHttpClientWithContext(ctx context.Context, httpClient *http.Client) (*BaseHttpClient, error) { + l := ctxzap.Extract(ctx) + disableCache, err := strconv.ParseBool(os.Getenv("BATON_DISABLE_HTTP_CACHE")) + if err != nil { + disableCache = false + } + cacheMaxSize, err := strconv.ParseInt(os.Getenv("BATON_HTTP_CACHE_MAX_SIZE"), 10, 64) + if err != nil { + cacheMaxSize = 128 // MB + } + cacheTTL, err := strconv.ParseInt(os.Getenv("BATON_HTTP_CACHE_TTL"), 10, 64) + if err != nil { + cacheTTL = 3600 // seconds + } + + var ( + config = CacheConfig{ + LogDebug: l.Level().Enabled(zap.DebugLevel), + CacheTTL: int32(cacheTTL), // seconds + CacheMaxSize: int(cacheMaxSize), // MB + DisableCache: disableCache, + } + ok bool + ) + if v := ctx.Value(ContextKey{}); v != nil { + if config, ok = v.(CacheConfig); !ok { + return nil, fmt.Errorf("error casting config values from context") + } + } + + cache, err := NewGoCache(ctx, config) + if err != nil { + l.Error("error creating http cache", zap.Error(err)) + return nil, err + } + + return &BaseHttpClient{ + HttpClient: httpClient, + baseHttpCache: cache, + }, nil +} + +// WithJSONResponse is a wrapper that marshals the returned response body into +// the provided shape. If the API should return an empty JSON body (i.e. HTTP +// status code 204 No Content), then pass a `nil` to `response`. func WithJSONResponse(response interface{}) DoOption { return func(resp *WrapperResponse) error { if !helpers.IsJSONContentType(resp.Header.Get(ContentType)) { return fmt.Errorf("unexpected content type for json response: %s", resp.Header.Get(ContentType)) } - return json.Unmarshal(resp.Body, response) + if response == nil && len(resp.Body) == 0 { + return nil + } + err := json.Unmarshal(resp.Body, response) + if err != nil { + return fmt.Errorf("failed to unmarshal json response: %w. body %v", err, resp.Body) + } + return nil } } @@ -101,7 +176,14 @@ func WithXMLResponse(response interface{}) DoOption { if !helpers.IsXMLContentType(resp.Header.Get(ContentType)) { return fmt.Errorf("unexpected content type for xml response: %s", resp.Header.Get(ContentType)) } - return xml.Unmarshal(resp.Body, response) + if response == nil && len(resp.Body) == 0 { + return nil + } + err := xml.Unmarshal(resp.Body, response) + if err != nil { + return fmt.Errorf("failed to unmarshal xml response: %w. body %v", err, resp.Body) + } + return nil } } @@ -119,16 +201,47 @@ func WithResponse(response interface{}) DoOption { } func (c *BaseHttpClient) Do(req *http.Request, options ...DoOption) (*http.Response, error) { - resp, err := c.HttpClient.Do(req) - if err != nil { - return nil, err + var ( + cacheKey string + err error + resp *http.Response + ) + l := ctxzap.Extract(req.Context()) + if req.Method == http.MethodGet { + cacheKey, err = CreateCacheKey(req) + if err != nil { + return nil, err + } + + resp, err = c.baseHttpCache.Get(cacheKey) + if err != nil { + return nil, err + } + if resp == nil { + l.Debug("http cache miss", zap.String("cacheKey", cacheKey), zap.String("url", req.URL.String())) + } else { + l.Debug("http cache hit", zap.String("cacheKey", cacheKey), zap.String("url", req.URL.String())) + } } - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err + if resp == nil { + resp, err = c.HttpClient.Do(req) + if err != nil { + var urlErr *url.Error + if errors.As(err, &urlErr) { + if urlErr.Timeout() { + return nil, status.Error(codes.DeadlineExceeded, fmt.Sprintf("request timeout: %v", urlErr.URL)) + } + } + if errors.Is(err, context.DeadlineExceeded) { + return nil, status.Error(codes.DeadlineExceeded, "request timeout") + } + return nil, err + } } - err = resp.Body.Close() + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } @@ -150,6 +263,8 @@ func (c *BaseHttpClient) Do(req *http.Request, options ...DoOption) (*http.Respo } switch resp.StatusCode { + case http.StatusRequestTimeout: + return resp, status.Error(codes.DeadlineExceeded, resp.Status) case http.StatusTooManyRequests: return resp, status.Error(codes.Unavailable, resp.Status) case http.StatusNotFound: @@ -166,6 +281,14 @@ func (c *BaseHttpClient) Do(req *http.Request, options ...DoOption) (*http.Respo return resp, status.Error(codes.Unknown, fmt.Sprintf("unexpected status code: %d", resp.StatusCode)) } + if req.Method == http.MethodGet && resp.StatusCode == http.StatusOK { + err := c.baseHttpCache.Set(cacheKey, resp) + if err != nil { + l.Debug("error setting cache", zap.String("cacheKey", cacheKey), zap.String("url", req.URL.String()), zap.Error(err)) + return resp, err + } + } + return resp, err } @@ -194,16 +317,53 @@ func WithJSONBody(body interface{}) RequestOption { } } +func WithFormBody(body string) RequestOption { + return func() (io.ReadWriter, map[string]string, error) { + var buffer bytes.Buffer + _, err := buffer.WriteString(body) + if err != nil { + return nil, nil, err + } + + _, headers, err := WithContentTypeFormHeader()() + if err != nil { + return nil, nil, err + } + + return &buffer, headers, nil + } +} + func WithAcceptJSONHeader() RequestOption { - return WithHeader("Accept", "application/json") + return WithAccept(applicationJSON) } func WithContentTypeJSONHeader() RequestOption { - return WithHeader("Content-Type", "application/json") + return WithContentType(applicationJSON) } func WithAcceptXMLHeader() RequestOption { - return WithHeader("Accept", "application/xml") + return WithAccept(applicationXML) +} + +func WithContentTypeFormHeader() RequestOption { + return WithContentType(applicationFormUrlencoded) +} + +func WithContentTypeVndHeader() RequestOption { + return WithContentType(applicationVndApiJSON) +} + +func WithAcceptVndJSONHeader() RequestOption { + return WithAccept(applicationVndApiJSON) +} + +func WithContentType(ctype string) RequestOption { + return WithHeader(ContentType, ctype) +} + +func WithAccept(value string) RequestOption { + return WithHeader(acceptHeader, value) } func (c *BaseHttpClient) NewRequest(ctx context.Context, method string, url *url.URL, options ...RequestOption) (*http.Request, error) { diff --git a/vendor/github.com/deckarep/golang-set/v2/.gitignore b/vendor/github.com/deckarep/golang-set/v2/.gitignore new file mode 100644 index 00000000..4eb156d1 --- /dev/null +++ b/vendor/github.com/deckarep/golang-set/v2/.gitignore @@ -0,0 +1,23 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +.idea \ No newline at end of file diff --git a/vendor/github.com/deckarep/golang-set/v2/LICENSE b/vendor/github.com/deckarep/golang-set/v2/LICENSE new file mode 100644 index 00000000..efd4827e --- /dev/null +++ b/vendor/github.com/deckarep/golang-set/v2/LICENSE @@ -0,0 +1,22 @@ +Open Source Initiative OSI - The MIT License (MIT):Licensing + +The MIT License (MIT) +Copyright (c) 2013 - 2022 Ralph Caraveo (deckarep@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/deckarep/golang-set/v2/README.md b/vendor/github.com/deckarep/golang-set/v2/README.md new file mode 100644 index 00000000..921f0cec --- /dev/null +++ b/vendor/github.com/deckarep/golang-set/v2/README.md @@ -0,0 +1,181 @@ +![example workflow](https://github.com/deckarep/golang-set/actions/workflows/ci.yml/badge.svg) +[![Go Report Card](https://goreportcard.com/badge/github.com/deckarep/golang-set/v2)](https://goreportcard.com/report/github.com/deckarep/golang-set/v2) +[![GoDoc](https://godoc.org/github.com/deckarep/golang-set/v2?status.svg)](http://godoc.org/github.com/deckarep/golang-set/v2) + +# golang-set + +The missing `generic` set collection for the Go language. Until Go has sets built-in...use this. + +## Update 3/5/2023 +* Packaged version: `2.2.0` release includes a refactor to minimize pointer indirection, better method documentation standards and a few constructor convenience methods to increase ergonomics when appending items `Append` or creating a new set from an exist `Map`. +* supports `new generic` syntax +* Go `1.18.0` or higher +* Workflow tested on Go `1.20` + +![With Generics](new_improved.jpeg) + +Coming from Python one of the things I miss is the superbly wonderful set collection. This is my attempt to mimic the primary features of the set collection from Python. +You can of course argue that there is no need for a set in Go, otherwise the creators would have added one to the standard library. To those I say simply ignore this repository and carry-on and to the rest that find this useful please contribute in helping me make it better by contributing with suggestions or PRs. + +## Install + +Use `go get` to install this package. + +```shell +go get github.com/deckarep/golang-set/v2 +``` + +## Features + +* *NEW* [Generics](https://go.dev/doc/tutorial/generics) based implementation (requires [Go 1.18](https://go.dev/blog/go1.18beta1) or higher) +* One common *interface* to both implementations + * a **non threadsafe** implementation favoring *performance* + * a **threadsafe** implementation favoring *concurrent* use +* Feature complete set implementation modeled after [Python's set implementation](https://docs.python.org/3/library/stdtypes.html#set). +* Exhaustive unit-test and benchmark suite + +## Trusted by + +This package is trusted by many companies and thousands of open-source packages. Here are just a few sample users of this package. + +* Notable projects/companies using this package + * Ethereum + * Docker + * 1Password + * Hashicorp + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=deckarep/golang-set&type=Date)](https://star-history.com/#deckarep/golang-set&Date) + + +## Usage + +The code below demonstrates how a Set collection can better manage data and actually minimize boilerplate and needless loops in code. This package now fully supports *generic* syntax so you are now able to instantiate a collection for any [comparable](https://flaviocopes.com/golang-comparing-values/) type object. + +What is considered comparable in Go? +* `Booleans`, `integers`, `strings`, `floats` or basically primitive types. +* `Pointers` +* `Arrays` +* `Structs` if *all of their fields* are also comparable independently + +Using this library is as simple as creating either a threadsafe or non-threadsafe set and providing a `comparable` type for instantiation of the collection. + +```go +// Syntax example, doesn't compile. +mySet := mapset.NewSet[T]() // where T is some concrete comparable type. + +// Therefore this code creates an int set +mySet := mapset.NewSet[int]() + +// Or perhaps you want a string set +mySet := mapset.NewSet[string]() + +type myStruct struct { + name string + age uint8 +} + +// Alternatively a set of structs +mySet := mapset.NewSet[myStruct]() + +// Lastly a set that can hold anything using the any or empty interface keyword: interface{}. This is effectively removes type safety. +mySet := mapset.NewSet[any]() +``` + +## Comprehensive Example + +```go +package main + +import ( + "fmt" + mapset "github.com/deckarep/golang-set/v2" +) + +func main() { + // Create a string-based set of required classes. + required := mapset.NewSet[string]() + required.Add("cooking") + required.Add("english") + required.Add("math") + required.Add("biology") + + // Create a string-based set of science classes. + sciences := mapset.NewSet[string]() + sciences.Add("biology") + sciences.Add("chemistry") + + // Create a string-based set of electives. + electives := mapset.NewSet[string]() + electives.Add("welding") + electives.Add("music") + electives.Add("automotive") + + // Create a string-based set of bonus programming classes. + bonus := mapset.NewSet[string]() + bonus.Add("beginner go") + bonus.Add("python for dummies") +} +``` + +Create a set of all unique classes. +Sets will *automatically* deduplicate the same data. + +```go + all := required + .Union(sciences) + .Union(electives) + .Union(bonus) + + fmt.Println(all) +``` + +Output: +```sh +Set{cooking, english, math, chemistry, welding, biology, music, automotive, beginner go, python for dummies} +``` + +Is cooking considered a science class? +```go +result := sciences.Contains("cooking") +fmt.Println(result) +``` + +Output: +```false +false +``` + +Show me all classes that are not science classes, since I don't enjoy science. +```go +notScience := all.Difference(sciences) +fmt.Println(notScience) +``` + +```sh +Set{ music, automotive, beginner go, python for dummies, cooking, english, math, welding } +``` + +Which science classes are also required classes? +```go +reqScience := sciences.Intersect(required) +``` + +Output: +```sh +Set{biology} +``` + +How many bonus classes do you offer? +```go +fmt.Println(bonus.Cardinality()) +``` +Output: +```sh +2 +``` + +Thanks for visiting! + +-deckarep diff --git a/vendor/github.com/deckarep/golang-set/v2/iterator.go b/vendor/github.com/deckarep/golang-set/v2/iterator.go new file mode 100644 index 00000000..fc14e705 --- /dev/null +++ b/vendor/github.com/deckarep/golang-set/v2/iterator.go @@ -0,0 +1,58 @@ +/* +Open Source Initiative OSI - The MIT License (MIT):Licensing + +The MIT License (MIT) +Copyright (c) 2013 - 2022 Ralph Caraveo (deckarep@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package mapset + +// Iterator defines an iterator over a Set, its C channel can be used to range over the Set's +// elements. +type Iterator[T comparable] struct { + C <-chan T + stop chan struct{} +} + +// Stop stops the Iterator, no further elements will be received on C, C will be closed. +func (i *Iterator[T]) Stop() { + // Allows for Stop() to be called multiple times + // (close() panics when called on already closed channel) + defer func() { + recover() + }() + + close(i.stop) + + // Exhaust any remaining elements. + for range i.C { + } +} + +// newIterator returns a new Iterator instance together with its item and stop channels. +func newIterator[T comparable]() (*Iterator[T], chan<- T, <-chan struct{}) { + itemChan := make(chan T) + stopChan := make(chan struct{}) + return &Iterator[T]{ + C: itemChan, + stop: stopChan, + }, itemChan, stopChan +} diff --git a/vendor/github.com/deckarep/golang-set/v2/new_improved.jpeg b/vendor/github.com/deckarep/golang-set/v2/new_improved.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..429752a07a94b837434515275aa7645714879341 GIT binary patch literal 120935 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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/deckarep/golang-set/v2/set.go b/vendor/github.com/deckarep/golang-set/v2/set.go new file mode 100644 index 00000000..292089dc --- /dev/null +++ b/vendor/github.com/deckarep/golang-set/v2/set.go @@ -0,0 +1,255 @@ +/* +Open Source Initiative OSI - The MIT License (MIT):Licensing + +The MIT License (MIT) +Copyright (c) 2013 - 2022 Ralph Caraveo (deckarep@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Package mapset implements a simple and set collection. +// Items stored within it are unordered and unique. It supports +// typical set operations: membership testing, intersection, union, +// difference, symmetric difference and cloning. +// +// Package mapset provides two implementations of the Set +// interface. The default implementation is safe for concurrent +// access, but a non-thread-safe implementation is also provided for +// programs that can benefit from the slight speed improvement and +// that can enforce mutual exclusion through other means. +package mapset + +// Set is the primary interface provided by the mapset package. It +// represents an unordered set of data and a large number of +// operations that can be applied to that set. +type Set[T comparable] interface { + // Add adds an element to the set. Returns whether + // the item was added. + Add(val T) bool + + // Append multiple elements to the set. Returns + // the number of elements added. + Append(val ...T) int + + // Cardinality returns the number of elements in the set. + Cardinality() int + + // Clear removes all elements from the set, leaving + // the empty set. + Clear() + + // Clone returns a clone of the set using the same + // implementation, duplicating all keys. + Clone() Set[T] + + // Contains returns whether the given items + // are all in the set. + Contains(val ...T) bool + + // ContainsOne returns whether the given item + // is in the set. + // + // Contains may cause the argument to escape to the heap. + // See: https://github.com/deckarep/golang-set/issues/118 + ContainsOne(val T) bool + + // ContainsAny returns whether at least one of the + // given items are in the set. + ContainsAny(val ...T) bool + + // Difference returns the difference between this set + // and other. The returned set will contain + // all elements of this set that are not also + // elements of other. + // + // Note that the argument to Difference + // must be of the same type as the receiver + // of the method. Otherwise, Difference will + // panic. + Difference(other Set[T]) Set[T] + + // Equal determines if two sets are equal to each + // other. If they have the same cardinality + // and contain the same elements, they are + // considered equal. The order in which + // the elements were added is irrelevant. + // + // Note that the argument to Equal must be + // of the same type as the receiver of the + // method. Otherwise, Equal will panic. + Equal(other Set[T]) bool + + // Intersect returns a new set containing only the elements + // that exist only in both sets. + // + // Note that the argument to Intersect + // must be of the same type as the receiver + // of the method. Otherwise, Intersect will + // panic. + Intersect(other Set[T]) Set[T] + + // IsEmpty determines if there are elements in the set. + IsEmpty() bool + + // IsProperSubset determines if every element in this set is in + // the other set but the two sets are not equal. + // + // Note that the argument to IsProperSubset + // must be of the same type as the receiver + // of the method. Otherwise, IsProperSubset + // will panic. + IsProperSubset(other Set[T]) bool + + // IsProperSuperset determines if every element in the other set + // is in this set but the two sets are not + // equal. + // + // Note that the argument to IsSuperset + // must be of the same type as the receiver + // of the method. Otherwise, IsSuperset will + // panic. + IsProperSuperset(other Set[T]) bool + + // IsSubset determines if every element in this set is in + // the other set. + // + // Note that the argument to IsSubset + // must be of the same type as the receiver + // of the method. Otherwise, IsSubset will + // panic. + IsSubset(other Set[T]) bool + + // IsSuperset determines if every element in the other set + // is in this set. + // + // Note that the argument to IsSuperset + // must be of the same type as the receiver + // of the method. Otherwise, IsSuperset will + // panic. + IsSuperset(other Set[T]) bool + + // Each iterates over elements and executes the passed func against each element. + // If passed func returns true, stop iteration at the time. + Each(func(T) bool) + + // Iter returns a channel of elements that you can + // range over. + Iter() <-chan T + + // Iterator returns an Iterator object that you can + // use to range over the set. + Iterator() *Iterator[T] + + // Remove removes a single element from the set. + Remove(i T) + + // RemoveAll removes multiple elements from the set. + RemoveAll(i ...T) + + // String provides a convenient string representation + // of the current state of the set. + String() string + + // SymmetricDifference returns a new set with all elements which are + // in either this set or the other set but not in both. + // + // Note that the argument to SymmetricDifference + // must be of the same type as the receiver + // of the method. Otherwise, SymmetricDifference + // will panic. + SymmetricDifference(other Set[T]) Set[T] + + // Union returns a new set with all elements in both sets. + // + // Note that the argument to Union must be of the + // same type as the receiver of the method. + // Otherwise, Union will panic. + Union(other Set[T]) Set[T] + + // Pop removes and returns an arbitrary item from the set. + Pop() (T, bool) + + // ToSlice returns the members of the set as a slice. + ToSlice() []T + + // MarshalJSON will marshal the set into a JSON-based representation. + MarshalJSON() ([]byte, error) + + // UnmarshalJSON will unmarshal a JSON-based byte slice into a full Set datastructure. + // For this to work, set subtypes must implemented the Marshal/Unmarshal interface. + UnmarshalJSON(b []byte) error +} + +// NewSet creates and returns a new set with the given elements. +// Operations on the resulting set are thread-safe. +func NewSet[T comparable](vals ...T) Set[T] { + s := newThreadSafeSetWithSize[T](len(vals)) + for _, item := range vals { + s.Add(item) + } + return s +} + +// NewSetWithSize creates and returns a reference to an empty set with a specified +// capacity. Operations on the resulting set are thread-safe. +func NewSetWithSize[T comparable](cardinality int) Set[T] { + s := newThreadSafeSetWithSize[T](cardinality) + return s +} + +// NewThreadUnsafeSet creates and returns a new set with the given elements. +// Operations on the resulting set are not thread-safe. +func NewThreadUnsafeSet[T comparable](vals ...T) Set[T] { + s := newThreadUnsafeSetWithSize[T](len(vals)) + for _, item := range vals { + s.Add(item) + } + return s +} + +// NewThreadUnsafeSetWithSize creates and returns a reference to an empty set with +// a specified capacity. Operations on the resulting set are not thread-safe. +func NewThreadUnsafeSetWithSize[T comparable](cardinality int) Set[T] { + s := newThreadUnsafeSetWithSize[T](cardinality) + return s +} + +// NewSetFromMapKeys creates and returns a new set with the given keys of the map. +// Operations on the resulting set are thread-safe. +func NewSetFromMapKeys[T comparable, V any](val map[T]V) Set[T] { + s := NewSetWithSize[T](len(val)) + + for k := range val { + s.Add(k) + } + + return s +} + +// NewThreadUnsafeSetFromMapKeys creates and returns a new set with the given keys of the map. +// Operations on the resulting set are not thread-safe. +func NewThreadUnsafeSetFromMapKeys[T comparable, V any](val map[T]V) Set[T] { + s := NewThreadUnsafeSetWithSize[T](len(val)) + + for k := range val { + s.Add(k) + } + + return s +} diff --git a/vendor/github.com/deckarep/golang-set/v2/sorted.go b/vendor/github.com/deckarep/golang-set/v2/sorted.go new file mode 100644 index 00000000..8ee2e707 --- /dev/null +++ b/vendor/github.com/deckarep/golang-set/v2/sorted.go @@ -0,0 +1,42 @@ +//go:build go1.21 +// +build go1.21 + +/* +Open Source Initiative OSI - The MIT License (MIT):Licensing + +The MIT License (MIT) +Copyright (c) 2013 - 2023 Ralph Caraveo (deckarep@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package mapset + +import ( + "cmp" + "slices" +) + +// Sorted returns a sorted slice of a set of any ordered type in ascending order. +// When sorting floating-point numbers, NaNs are ordered before other values. +func Sorted[E cmp.Ordered](set Set[E]) []E { + s := set.ToSlice() + slices.Sort(s) + return s +} diff --git a/vendor/github.com/deckarep/golang-set/v2/threadsafe.go b/vendor/github.com/deckarep/golang-set/v2/threadsafe.go new file mode 100644 index 00000000..ad7a834b --- /dev/null +++ b/vendor/github.com/deckarep/golang-set/v2/threadsafe.go @@ -0,0 +1,299 @@ +/* +Open Source Initiative OSI - The MIT License (MIT):Licensing + +The MIT License (MIT) +Copyright (c) 2013 - 2022 Ralph Caraveo (deckarep@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package mapset + +import "sync" + +type threadSafeSet[T comparable] struct { + sync.RWMutex + uss threadUnsafeSet[T] +} + +func newThreadSafeSet[T comparable]() *threadSafeSet[T] { + return &threadSafeSet[T]{ + uss: newThreadUnsafeSet[T](), + } +} + +func newThreadSafeSetWithSize[T comparable](cardinality int) *threadSafeSet[T] { + return &threadSafeSet[T]{ + uss: newThreadUnsafeSetWithSize[T](cardinality), + } +} + +func (t *threadSafeSet[T]) Add(v T) bool { + t.Lock() + ret := t.uss.Add(v) + t.Unlock() + return ret +} + +func (t *threadSafeSet[T]) Append(v ...T) int { + t.Lock() + ret := t.uss.Append(v...) + t.Unlock() + return ret +} + +func (t *threadSafeSet[T]) Contains(v ...T) bool { + t.RLock() + ret := t.uss.Contains(v...) + t.RUnlock() + + return ret +} + +func (t *threadSafeSet[T]) ContainsOne(v T) bool { + t.RLock() + ret := t.uss.ContainsOne(v) + t.RUnlock() + + return ret +} + +func (t *threadSafeSet[T]) ContainsAny(v ...T) bool { + t.RLock() + ret := t.uss.ContainsAny(v...) + t.RUnlock() + + return ret +} + +func (t *threadSafeSet[T]) IsEmpty() bool { + return t.Cardinality() == 0 +} + +func (t *threadSafeSet[T]) IsSubset(other Set[T]) bool { + o := other.(*threadSafeSet[T]) + + t.RLock() + o.RLock() + + ret := t.uss.IsSubset(o.uss) + t.RUnlock() + o.RUnlock() + return ret +} + +func (t *threadSafeSet[T]) IsProperSubset(other Set[T]) bool { + o := other.(*threadSafeSet[T]) + + t.RLock() + defer t.RUnlock() + o.RLock() + defer o.RUnlock() + + return t.uss.IsProperSubset(o.uss) +} + +func (t *threadSafeSet[T]) IsSuperset(other Set[T]) bool { + return other.IsSubset(t) +} + +func (t *threadSafeSet[T]) IsProperSuperset(other Set[T]) bool { + return other.IsProperSubset(t) +} + +func (t *threadSafeSet[T]) Union(other Set[T]) Set[T] { + o := other.(*threadSafeSet[T]) + + t.RLock() + o.RLock() + + unsafeUnion := t.uss.Union(o.uss).(threadUnsafeSet[T]) + ret := &threadSafeSet[T]{uss: unsafeUnion} + t.RUnlock() + o.RUnlock() + return ret +} + +func (t *threadSafeSet[T]) Intersect(other Set[T]) Set[T] { + o := other.(*threadSafeSet[T]) + + t.RLock() + o.RLock() + + unsafeIntersection := t.uss.Intersect(o.uss).(threadUnsafeSet[T]) + ret := &threadSafeSet[T]{uss: unsafeIntersection} + t.RUnlock() + o.RUnlock() + return ret +} + +func (t *threadSafeSet[T]) Difference(other Set[T]) Set[T] { + o := other.(*threadSafeSet[T]) + + t.RLock() + o.RLock() + + unsafeDifference := t.uss.Difference(o.uss).(threadUnsafeSet[T]) + ret := &threadSafeSet[T]{uss: unsafeDifference} + t.RUnlock() + o.RUnlock() + return ret +} + +func (t *threadSafeSet[T]) SymmetricDifference(other Set[T]) Set[T] { + o := other.(*threadSafeSet[T]) + + t.RLock() + o.RLock() + + unsafeDifference := t.uss.SymmetricDifference(o.uss).(threadUnsafeSet[T]) + ret := &threadSafeSet[T]{uss: unsafeDifference} + t.RUnlock() + o.RUnlock() + return ret +} + +func (t *threadSafeSet[T]) Clear() { + t.Lock() + t.uss.Clear() + t.Unlock() +} + +func (t *threadSafeSet[T]) Remove(v T) { + t.Lock() + delete(t.uss, v) + t.Unlock() +} + +func (t *threadSafeSet[T]) RemoveAll(i ...T) { + t.Lock() + t.uss.RemoveAll(i...) + t.Unlock() +} + +func (t *threadSafeSet[T]) Cardinality() int { + t.RLock() + defer t.RUnlock() + return len(t.uss) +} + +func (t *threadSafeSet[T]) Each(cb func(T) bool) { + t.RLock() + for elem := range t.uss { + if cb(elem) { + break + } + } + t.RUnlock() +} + +func (t *threadSafeSet[T]) Iter() <-chan T { + ch := make(chan T) + go func() { + t.RLock() + + for elem := range t.uss { + ch <- elem + } + close(ch) + t.RUnlock() + }() + + return ch +} + +func (t *threadSafeSet[T]) Iterator() *Iterator[T] { + iterator, ch, stopCh := newIterator[T]() + + go func() { + t.RLock() + L: + for elem := range t.uss { + select { + case <-stopCh: + break L + case ch <- elem: + } + } + close(ch) + t.RUnlock() + }() + + return iterator +} + +func (t *threadSafeSet[T]) Equal(other Set[T]) bool { + o := other.(*threadSafeSet[T]) + + t.RLock() + o.RLock() + + ret := t.uss.Equal(o.uss) + t.RUnlock() + o.RUnlock() + return ret +} + +func (t *threadSafeSet[T]) Clone() Set[T] { + t.RLock() + + unsafeClone := t.uss.Clone().(threadUnsafeSet[T]) + ret := &threadSafeSet[T]{uss: unsafeClone} + t.RUnlock() + return ret +} + +func (t *threadSafeSet[T]) String() string { + t.RLock() + ret := t.uss.String() + t.RUnlock() + return ret +} + +func (t *threadSafeSet[T]) Pop() (T, bool) { + t.Lock() + defer t.Unlock() + return t.uss.Pop() +} + +func (t *threadSafeSet[T]) ToSlice() []T { + keys := make([]T, 0, t.Cardinality()) + t.RLock() + for elem := range t.uss { + keys = append(keys, elem) + } + t.RUnlock() + return keys +} + +func (t *threadSafeSet[T]) MarshalJSON() ([]byte, error) { + t.RLock() + b, err := t.uss.MarshalJSON() + t.RUnlock() + + return b, err +} + +func (t *threadSafeSet[T]) UnmarshalJSON(p []byte) error { + t.RLock() + err := t.uss.UnmarshalJSON(p) + t.RUnlock() + + return err +} diff --git a/vendor/github.com/deckarep/golang-set/v2/threadunsafe.go b/vendor/github.com/deckarep/golang-set/v2/threadunsafe.go new file mode 100644 index 00000000..8b17b017 --- /dev/null +++ b/vendor/github.com/deckarep/golang-set/v2/threadunsafe.go @@ -0,0 +1,330 @@ +/* +Open Source Initiative OSI - The MIT License (MIT):Licensing + +The MIT License (MIT) +Copyright (c) 2013 - 2022 Ralph Caraveo (deckarep@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package mapset + +import ( + "encoding/json" + "fmt" + "strings" +) + +type threadUnsafeSet[T comparable] map[T]struct{} + +// Assert concrete type:threadUnsafeSet adheres to Set interface. +var _ Set[string] = (threadUnsafeSet[string])(nil) + +func newThreadUnsafeSet[T comparable]() threadUnsafeSet[T] { + return make(threadUnsafeSet[T]) +} + +func newThreadUnsafeSetWithSize[T comparable](cardinality int) threadUnsafeSet[T] { + return make(threadUnsafeSet[T], cardinality) +} + +func (s threadUnsafeSet[T]) Add(v T) bool { + prevLen := len(s) + s[v] = struct{}{} + return prevLen != len(s) +} + +func (s threadUnsafeSet[T]) Append(v ...T) int { + prevLen := len(s) + for _, val := range v { + (s)[val] = struct{}{} + } + return len(s) - prevLen +} + +// private version of Add which doesn't return a value +func (s threadUnsafeSet[T]) add(v T) { + s[v] = struct{}{} +} + +func (s threadUnsafeSet[T]) Cardinality() int { + return len(s) +} + +func (s threadUnsafeSet[T]) Clear() { + // Constructions like this are optimised by compiler, and replaced by + // mapclear() function, defined in + // https://github.com/golang/go/blob/29bbca5c2c1ad41b2a9747890d183b6dd3a4ace4/src/runtime/map.go#L993) + for key := range s { + delete(s, key) + } +} + +func (s threadUnsafeSet[T]) Clone() Set[T] { + clonedSet := newThreadUnsafeSetWithSize[T](s.Cardinality()) + for elem := range s { + clonedSet.add(elem) + } + return clonedSet +} + +func (s threadUnsafeSet[T]) Contains(v ...T) bool { + for _, val := range v { + if _, ok := s[val]; !ok { + return false + } + } + return true +} + +func (s threadUnsafeSet[T]) ContainsOne(v T) bool { + _, ok := s[v] + return ok +} + +func (s threadUnsafeSet[T]) ContainsAny(v ...T) bool { + for _, val := range v { + if _, ok := s[val]; ok { + return true + } + } + return false +} + +// private version of Contains for a single element v +func (s threadUnsafeSet[T]) contains(v T) (ok bool) { + _, ok = s[v] + return ok +} + +func (s threadUnsafeSet[T]) Difference(other Set[T]) Set[T] { + o := other.(threadUnsafeSet[T]) + + diff := newThreadUnsafeSet[T]() + for elem := range s { + if !o.contains(elem) { + diff.add(elem) + } + } + return diff +} + +func (s threadUnsafeSet[T]) Each(cb func(T) bool) { + for elem := range s { + if cb(elem) { + break + } + } +} + +func (s threadUnsafeSet[T]) Equal(other Set[T]) bool { + o := other.(threadUnsafeSet[T]) + + if s.Cardinality() != other.Cardinality() { + return false + } + for elem := range s { + if !o.contains(elem) { + return false + } + } + return true +} + +func (s threadUnsafeSet[T]) Intersect(other Set[T]) Set[T] { + o := other.(threadUnsafeSet[T]) + + intersection := newThreadUnsafeSet[T]() + // loop over smaller set + if s.Cardinality() < other.Cardinality() { + for elem := range s { + if o.contains(elem) { + intersection.add(elem) + } + } + } else { + for elem := range o { + if s.contains(elem) { + intersection.add(elem) + } + } + } + return intersection +} + +func (s threadUnsafeSet[T]) IsEmpty() bool { + return s.Cardinality() == 0 +} + +func (s threadUnsafeSet[T]) IsProperSubset(other Set[T]) bool { + return s.Cardinality() < other.Cardinality() && s.IsSubset(other) +} + +func (s threadUnsafeSet[T]) IsProperSuperset(other Set[T]) bool { + return s.Cardinality() > other.Cardinality() && s.IsSuperset(other) +} + +func (s threadUnsafeSet[T]) IsSubset(other Set[T]) bool { + o := other.(threadUnsafeSet[T]) + if s.Cardinality() > other.Cardinality() { + return false + } + for elem := range s { + if !o.contains(elem) { + return false + } + } + return true +} + +func (s threadUnsafeSet[T]) IsSuperset(other Set[T]) bool { + return other.IsSubset(s) +} + +func (s threadUnsafeSet[T]) Iter() <-chan T { + ch := make(chan T) + go func() { + for elem := range s { + ch <- elem + } + close(ch) + }() + + return ch +} + +func (s threadUnsafeSet[T]) Iterator() *Iterator[T] { + iterator, ch, stopCh := newIterator[T]() + + go func() { + L: + for elem := range s { + select { + case <-stopCh: + break L + case ch <- elem: + } + } + close(ch) + }() + + return iterator +} + +// Pop returns a popped item in case set is not empty, or nil-value of T +// if set is already empty +func (s threadUnsafeSet[T]) Pop() (v T, ok bool) { + for item := range s { + delete(s, item) + return item, true + } + return v, false +} + +func (s threadUnsafeSet[T]) Remove(v T) { + delete(s, v) +} + +func (s threadUnsafeSet[T]) RemoveAll(i ...T) { + for _, elem := range i { + delete(s, elem) + } +} + +func (s threadUnsafeSet[T]) String() string { + items := make([]string, 0, len(s)) + + for elem := range s { + items = append(items, fmt.Sprintf("%v", elem)) + } + return fmt.Sprintf("Set{%s}", strings.Join(items, ", ")) +} + +func (s threadUnsafeSet[T]) SymmetricDifference(other Set[T]) Set[T] { + o := other.(threadUnsafeSet[T]) + + sd := newThreadUnsafeSet[T]() + for elem := range s { + if !o.contains(elem) { + sd.add(elem) + } + } + for elem := range o { + if !s.contains(elem) { + sd.add(elem) + } + } + return sd +} + +func (s threadUnsafeSet[T]) ToSlice() []T { + keys := make([]T, 0, s.Cardinality()) + for elem := range s { + keys = append(keys, elem) + } + + return keys +} + +func (s threadUnsafeSet[T]) Union(other Set[T]) Set[T] { + o := other.(threadUnsafeSet[T]) + + n := s.Cardinality() + if o.Cardinality() > n { + n = o.Cardinality() + } + unionedSet := make(threadUnsafeSet[T], n) + + for elem := range s { + unionedSet.add(elem) + } + for elem := range o { + unionedSet.add(elem) + } + return unionedSet +} + +// MarshalJSON creates a JSON array from the set, it marshals all elements +func (s threadUnsafeSet[T]) MarshalJSON() ([]byte, error) { + items := make([]string, 0, s.Cardinality()) + + for elem := range s { + b, err := json.Marshal(elem) + if err != nil { + return nil, err + } + + items = append(items, string(b)) + } + + return []byte(fmt.Sprintf("[%s]", strings.Join(items, ","))), nil +} + +// UnmarshalJSON recreates a set from a JSON array, it only decodes +// primitive types. Numbers are decoded as json.Number. +func (s threadUnsafeSet[T]) UnmarshalJSON(b []byte) error { + var i []T + err := json.Unmarshal(b, &i) + if err != nil { + return err + } + s.Append(i...) + + return nil +} diff --git a/vendor/github.com/doug-martin/goqu/v9/dialect/sqlite3/sqlite3.go b/vendor/github.com/doug-martin/goqu/v9/dialect/sqlite3/sqlite3.go new file mode 100644 index 00000000..40ddb2a5 --- /dev/null +++ b/vendor/github.com/doug-martin/goqu/v9/dialect/sqlite3/sqlite3.go @@ -0,0 +1,76 @@ +package sqlite3 + +import ( + "time" + + "github.com/doug-martin/goqu/v9" + "github.com/doug-martin/goqu/v9/exp" +) + +func DialectOptions() *goqu.SQLDialectOptions { + opts := goqu.DefaultDialectOptions() + + opts.SupportsReturn = false + opts.SupportsOrderByOnUpdate = true + opts.SupportsLimitOnUpdate = true + opts.SupportsOrderByOnDelete = true + opts.SupportsLimitOnDelete = true + opts.SupportsConflictUpdateWhere = false + opts.SupportsInsertIgnoreSyntax = true + opts.SupportsConflictTarget = true + opts.SupportsMultipleUpdateTables = false + opts.WrapCompoundsInParens = false + opts.SupportsDistinctOn = false + opts.SupportsWindowFunction = false + opts.SupportsLateral = false + + opts.PlaceHolderFragment = []byte("?") + opts.IncludePlaceholderNum = false + opts.QuoteRune = '`' + opts.DefaultValuesFragment = []byte("") + opts.True = []byte("1") + opts.False = []byte("0") + opts.TimeFormat = time.RFC3339Nano + opts.BooleanOperatorLookup = map[exp.BooleanOperation][]byte{ + exp.EqOp: []byte("="), + exp.NeqOp: []byte("!="), + exp.GtOp: []byte(">"), + exp.GteOp: []byte(">="), + exp.LtOp: []byte("<"), + exp.LteOp: []byte("<="), + exp.InOp: []byte("IN"), + exp.NotInOp: []byte("NOT IN"), + exp.IsOp: []byte("IS"), + exp.IsNotOp: []byte("IS NOT"), + exp.LikeOp: []byte("LIKE"), + exp.NotLikeOp: []byte("NOT LIKE"), + exp.ILikeOp: []byte("LIKE"), + exp.NotILikeOp: []byte("NOT LIKE"), + exp.RegexpLikeOp: []byte("REGEXP"), + exp.RegexpNotLikeOp: []byte("NOT REGEXP"), + exp.RegexpILikeOp: []byte("REGEXP"), + exp.RegexpNotILikeOp: []byte("NOT REGEXP"), + } + opts.UseLiteralIsBools = false + opts.BitwiseOperatorLookup = map[exp.BitwiseOperation][]byte{ + exp.BitwiseOrOp: []byte("|"), + exp.BitwiseAndOp: []byte("&"), + exp.BitwiseLeftShiftOp: []byte("<<"), + exp.BitwiseRightShiftOp: []byte(">>"), + } + opts.EscapedRunes = map[rune][]byte{ + '\'': []byte("''"), + } + opts.InsertIgnoreClause = []byte("INSERT OR IGNORE INTO ") + opts.ConflictFragment = []byte(" ON CONFLICT ") + opts.ConflictDoUpdateFragment = []byte(" DO UPDATE SET ") + opts.ConflictDoNothingFragment = []byte(" DO NOTHING ") + opts.ForUpdateFragment = []byte("") + opts.OfFragment = []byte("") + opts.NowaitFragment = []byte("") + return opts +} + +func init() { + goqu.RegisterDialect("sqlite3", DialectOptions()) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index e729865f..93155029 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -13,6 +13,10 @@ filippo.io/edwards25519/field ## explicit; go 1.18 github.com/BurntSushi/toml github.com/BurntSushi/toml/internal +# github.com/allegro/bigcache/v3 v3.1.0 +## explicit; go 1.16 +github.com/allegro/bigcache/v3 +github.com/allegro/bigcache/v3/queue # github.com/aws/aws-sdk-go-v2 v1.27.1 ## explicit; go 1.20 github.com/aws/aws-sdk-go-v2/aws @@ -147,7 +151,7 @@ github.com/benbjohnson/clock # github.com/cenkalti/backoff/v4 v4.3.0 ## explicit; go 1.18 github.com/cenkalti/backoff/v4 -# github.com/conductorone/baton-sdk v0.1.43 +# github.com/conductorone/baton-sdk v0.2.18 ## explicit; go 1.21 github.com/conductorone/baton-sdk/internal/connector github.com/conductorone/baton-sdk/pb/c1/c1z/v1 @@ -159,6 +163,7 @@ github.com/conductorone/baton-sdk/pb/c1/reader/v2 github.com/conductorone/baton-sdk/pb/c1/utls/v1 github.com/conductorone/baton-sdk/pkg/annotations github.com/conductorone/baton-sdk/pkg/cli +github.com/conductorone/baton-sdk/pkg/config github.com/conductorone/baton-sdk/pkg/connectorbuilder github.com/conductorone/baton-sdk/pkg/connectorrunner github.com/conductorone/baton-sdk/pkg/connectorstore @@ -169,6 +174,7 @@ github.com/conductorone/baton-sdk/pkg/dotc1z github.com/conductorone/baton-sdk/pkg/dotc1z/manager github.com/conductorone/baton-sdk/pkg/dotc1z/manager/local github.com/conductorone/baton-sdk/pkg/dotc1z/manager/s3 +github.com/conductorone/baton-sdk/pkg/field github.com/conductorone/baton-sdk/pkg/helpers github.com/conductorone/baton-sdk/pkg/logging github.com/conductorone/baton-sdk/pkg/metrics @@ -177,6 +183,7 @@ github.com/conductorone/baton-sdk/pkg/provisioner github.com/conductorone/baton-sdk/pkg/ratelimit github.com/conductorone/baton-sdk/pkg/sdk github.com/conductorone/baton-sdk/pkg/sync +github.com/conductorone/baton-sdk/pkg/sync/expand github.com/conductorone/baton-sdk/pkg/tasks github.com/conductorone/baton-sdk/pkg/tasks/c1api github.com/conductorone/baton-sdk/pkg/tasks/local @@ -190,9 +197,13 @@ github.com/conductorone/baton-sdk/pkg/ugrpc github.com/conductorone/baton-sdk/pkg/uhttp github.com/conductorone/baton-sdk/pkg/us3 github.com/conductorone/baton-sdk/pkg/utls +# github.com/deckarep/golang-set/v2 v2.6.0 +## explicit; go 1.18 +github.com/deckarep/golang-set/v2 # github.com/doug-martin/goqu/v9 v9.19.0 ## explicit; go 1.12 github.com/doug-martin/goqu/v9 +github.com/doug-martin/goqu/v9/dialect/sqlite3 github.com/doug-martin/goqu/v9/exec github.com/doug-martin/goqu/v9/exp github.com/doug-martin/goqu/v9/internal/errors From 5b14e5b074901632a9ee889e24680788ab887753 Mon Sep 17 00:00:00 2001 From: Jorge Javier Araya Navarro Date: Wed, 14 Aug 2024 16:44:35 -0600 Subject: [PATCH 2/3] Break line length --- cmd/baton-okta/config.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/baton-okta/config.go b/cmd/baton-okta/config.go index 09d40f80..90b83200 100644 --- a/cmd/baton-okta/config.go +++ b/cmd/baton-okta/config.go @@ -13,7 +13,8 @@ var ( syncInactivateApps = field.BoolField("sync-inactive-apps", field.WithDescription("Whether to sync inactive apps or not"), field.WithDefaultValue(true)) oktaProvisioning = field.BoolField("okta-provisioning") ciam = field.BoolField("ciam", field.WithDescription("Whether to run in CIAM mode or not. In CIAM mode, only roles and the users assigned to roles are synced")) - ciamEmailDomains = field.StringSliceField("ciam-email-domains", field.WithDescription("The email domains to use for CIAM mode. Any users that don't have an email address with one of the provided domains will be ignored, unless explicitly granted a role")) + ciamEmailDomains = field.StringSliceField("ciam-email-domains", + field.WithDescription("The email domains to use for CIAM mode. Any users that don't have an email address with one of the provided domains will be ignored, unless explicitly granted a role")) ) var relationships = []field.SchemaFieldRelationship{ From 80ea08b656ce56c93419095234df8df2952c743c Mon Sep 17 00:00:00 2001 From: Jorge Javier Araya Navarro Date: Fri, 16 Aug 2024 13:12:09 -0600 Subject: [PATCH 3/3] Change relationship constraint --- cmd/baton-okta/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/baton-okta/config.go b/cmd/baton-okta/config.go index 90b83200..b577cb6f 100644 --- a/cmd/baton-okta/config.go +++ b/cmd/baton-okta/config.go @@ -18,7 +18,7 @@ var ( ) var relationships = []field.SchemaFieldRelationship{ - field.FieldsRequiredTogether(oktaPrivateKeyId, oktaPrivateKey), + field.FieldsDependentOn([]field.SchemaField{oktaPrivateKey, oktaPrivateKey}, []field.SchemaField{oktaClientId}), field.FieldsMutuallyExclusive(apiToken, oktaClientId), field.FieldsAtLeastOneUsed(apiToken, oktaClientId), }