diff --git a/.gitignore b/.gitignore index 11d69e97b..7648b6f7c 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ wasm/*.wasm .DS_Store bin + +dist diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 20d2df109..21f1a66b2 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -14,9 +14,24 @@ builds: - darwin ldflags: - -s -w -X 'github.com/teamkeel/keel/runtime.Version={{.Version}}' -X 'github.com/teamkeel/keel/cmd.enabledDebugFlags=false' + - main: ./deploy/lambdas/runtime/cmd + id: "runtime-lambda" + binary: runtime-lambda + env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + ldflags: + - -s -w -X 'github.com/teamkeel/keel/runtime.Version={{.Version}}' archives: - - builds: - - keel + - id: keel + builds: + - keel + - id: runtime-lambda + builds: + - runtime-lambda release: github: owner: teamkeel @@ -24,4 +39,4 @@ release: mode: append prerelease: true checksum: - name_template: "checksums.txt" \ No newline at end of file + name_template: "checksums.txt" diff --git a/cmd/deploy.go b/cmd/deploy.go new file mode 100644 index 000000000..48d4388ee --- /dev/null +++ b/cmd/deploy.go @@ -0,0 +1,380 @@ +package cmd + +import ( + "fmt" + "path/filepath" + "regexp" + "strings" + "time" + + "context" + "os" + + "github.com/charmbracelet/bubbles/table" + "github.com/charmbracelet/lipgloss" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "github.com/teamkeel/keel/colors" + "github.com/teamkeel/keel/deploy" +) + +var flagDeployRuntimeBinary string +var flagDeployLogsSince time.Duration +var flagDeployLogsStart string + +var deployCommand = &cobra.Command{ + Use: "deploy", + Short: "Self-host your Keel app on AWS", + Run: func(cmd *cobra.Command, args []string) { + // list subcommands + _ = cmd.Help() + }, +} + +type ValidateDeployFlagsResult struct { + ProjectDir string + Env string + RuntimeBinary string +} + +var envRegexp = regexp.MustCompile(`^[a-z\-0-9]+$`) + +func validateDeployFlags(runtimeBinaryRequired bool) *ValidateDeployFlagsResult { + // ensure environment is set and valid + if flagEnvironment == "" { + fmt.Println("You must specify an environment using the --env flag") + return nil + } + if flagEnvironment == "development" || flagEnvironment == "test" { + fmt.Println("--env cannot be 'development' or 'test' as these are reserved for 'keel run' and 'keel test' respectively") + return nil + } + if !envRegexp.MatchString(flagEnvironment) { + fmt.Println("--env can only contain lower-case letters, dashes, and numbers") + return nil + } + + // ensure project dir is absolute + absProjectDir, err := filepath.Abs(flagProjectDir) + if err != nil { + panic(err) + } + + // ensure rntime binary is set and absolute (if not url) + // TODO: update this to default to use pull the binary from the Github release by deefault + if runtimeBinaryRequired && flagDeployRuntimeBinary == "" { + fmt.Println("--runtime-binary must currently be set, either to a URL or a local path - in the future this will default to fetching from the Github release assets for the current version") + return nil + } + runtimeBinary := flagDeployRuntimeBinary + if runtimeBinary != "" && !strings.HasPrefix(runtimeBinary, "http") { + runtimeBinary, err = filepath.Abs(runtimeBinary) + if err != nil { + panic(err) + } + } + + return &ValidateDeployFlagsResult{ + ProjectDir: absProjectDir, + Env: flagEnvironment, + RuntimeBinary: runtimeBinary, + } +} + +var deployBuildCommand = &cobra.Command{ + Use: "build", + Short: "Build the resources that are needed to deploy your app to AWS account", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + defer panicHandler() + + validated := validateDeployFlags(true) + if validated == nil { + os.Exit(1) + } + + _, err := deploy.Build(context.Background(), &deploy.BuildArgs{ + ProjectRoot: validated.ProjectDir, + Env: validated.Env, + RuntimeBinary: validated.RuntimeBinary, + }) + if err != nil { + fmt.Println("error running build", err) + os.Exit(1) + } + }, +} + +var deployUpCommand = &cobra.Command{ + Use: "up", + Short: "Deploy your app into your AWS account for the given --env", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + defer panicHandler() + + validated := validateDeployFlags(true) + if validated == nil { + os.Exit(1) + } + + err := deploy.Run(context.Background(), &deploy.RunArgs{ + Action: deploy.UpAction, + ProjectRoot: validated.ProjectDir, + Env: validated.Env, + RuntimeBinary: validated.RuntimeBinary, + }) + if err != nil { + os.Exit(1) + } + }, +} + +var deployRemoveCommand = &cobra.Command{ + Use: "remove", + Short: "Remove all resources in AWS for the given --env", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + defer panicHandler() + + validated := validateDeployFlags(true) + if validated == nil { + os.Exit(1) + } + + prompt := promptui.Prompt{ + Label: fmt.Sprintf("This command will permanently delete all resources in your AWS account for the environment '%s', including your database.", validated.Env), + IsConfirm: true, + } + + _, err := prompt.Run() + if err != nil { + if err == promptui.ErrAbort { + fmt.Println("| Aborting...") + return + } + panic(err) + } + + err = deploy.Run(context.Background(), &deploy.RunArgs{ + Action: deploy.RemoveAction, + ProjectRoot: validated.ProjectDir, + Env: validated.Env, + RuntimeBinary: validated.RuntimeBinary, + }) + if err != nil { + os.Exit(1) + } + }, +} + +var deployLogsCommand = &cobra.Command{ + Use: "logs", + Short: "View logs from a deployed environment", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + defer panicHandler() + + validated := validateDeployFlags(false) + if validated == nil { + os.Exit(1) + } + + if flagDeployLogsSince != 0 && flagDeployLogsStart != "" { + fmt.Println("only one of --since and --start can be provided") + os.Exit(1) + } + + startTime := time.Now() + if flagDeployLogsSince > 0 { + startTime = startTime.Add(-flagDeployLogsSince) + } else if flagDeployLogsStart != "" { + var err error + startTime, err = time.Parse(time.DateTime, flagDeployLogsStart) + if err != nil { + fmt.Println("--start has invalid value, must be in format 'YYYY-MM-DD HH:MM:SS'") + os.Exit(1) + } + } + + err := deploy.StreamLogs(context.Background(), &deploy.StreamLogsArgs{ + ProjectRoot: validated.ProjectDir, + Env: validated.Env, + StartTime: startTime, + }) + if err != nil { + os.Exit(1) + } + }, +} + +var deploySecretsCommand = &cobra.Command{ + Use: "secrets", + Short: "Manage secrets for self-hosted Keel apps", + Run: func(cmd *cobra.Command, args []string) { + // list subcommands + _ = cmd.Help() + }, +} + +var deploySecretsSetCommand = &cobra.Command{ + Use: "set MY_KEY 'my-value' --env my-env", + Args: cobra.ExactArgs(2), + Short: "Set a secret in your AWS account for the given environment", + Run: func(cmd *cobra.Command, args []string) { + validated := validateDeployFlags(false) + if validated == nil { + os.Exit(1) + } + + err := deploy.SetSecret(context.Background(), &deploy.SetSecretArgs{ + ProjectRoot: validated.ProjectDir, + Env: validated.Env, + Key: args[0], + Value: args[1], + }) + if err != nil { + os.Exit(1) + } + + fmt.Printf("%s secret '%s' set\n", deploy.IconTick, args[0]) + }, +} + +var deploySecretsGetCommand = &cobra.Command{ + Use: "get MY_KEY --env my-env", + Args: cobra.ExactArgs(1), + Short: "Get a secret from your AWS account for the given environment", + Run: func(cmd *cobra.Command, args []string) { + validated := validateDeployFlags(false) + if validated == nil { + os.Exit(1) + } + + param, err := deploy.GetSecret(context.Background(), &deploy.GetSecretArgs{ + ProjectRoot: validated.ProjectDir, + Env: validated.Env, + Key: args[0], + }) + if err != nil { + os.Exit(1) + } + + fmt.Println(*param.Value) + }, +} + +var deploySecretsListCommand = &cobra.Command{ + Use: "list --env my-env", + Args: cobra.NoArgs, + Short: "List the secrets set in your AWS account for the given environment", + Run: func(cmd *cobra.Command, args []string) { + validated := validateDeployFlags(false) + if validated == nil { + os.Exit(1) + } + + params, err := deploy.ListSecrets(context.Background(), &deploy.ListSecretsArgs{ + ProjectRoot: validated.ProjectDir, + Env: validated.Env, + }) + if err != nil { + os.Exit(1) + } + + var rows []table.Row + maxKeyLength := 0 + for _, p := range params { + parts := strings.Split(*p.Name, "/") + name := parts[len(parts)-1] + if len(name) > maxKeyLength { + maxKeyLength = len(name) + } + rows = append(rows, table.Row{parts[len(parts)-1], *p.Value}) + } + + columns := []table.Column{ + {Title: "Name", Width: maxKeyLength}, + {Title: "Value", Width: 80}, + } + + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithHeight(len(params)), + ) + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.NoColor{}). + Bold(false) + s.Cell = s.Cell. + Foreground(colors.HighlightWhiteBright) + + t.SetStyles(s) + + secretsStyle := lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()) + + fmt.Println(secretsStyle.Render(t.View())) + }, +} + +var deploySecretsDeleteCommand = &cobra.Command{ + Use: "delete MY_KEY --env my-env", + Args: cobra.ExactArgs(1), + Short: "Delete a secret set in your AWS account for the given environment", + Run: func(cmd *cobra.Command, args []string) { + validated := validateDeployFlags(false) + if validated == nil { + os.Exit(1) + } + + err := deploy.DeleteSecret(context.Background(), &deploy.DeleteSecretArgs{ + ProjectRoot: validated.ProjectDir, + Env: validated.Env, + Key: args[0], + }) + if err != nil { + os.Exit(1) + } + + fmt.Printf("%s secret '%s' deleted\n", deploy.IconTick, args[0]) + }, +} + +func init() { + rootCmd.AddCommand(deployCommand) + + deployCommand.AddCommand(deployBuildCommand) + deployBuildCommand.Flags().StringVar(&flagEnvironment, "env", "", "The environment to build for e.g. staging or production") + deployBuildCommand.Flags().StringVar(&flagDeployRuntimeBinary, "runtime-binary", "", "") + + deployCommand.AddCommand(deployUpCommand) + deployUpCommand.Flags().StringVar(&flagEnvironment, "env", "", "The environment to deploy e.g. staging or production") + deployUpCommand.Flags().StringVar(&flagDeployRuntimeBinary, "runtime-binary", "", "") + + deployCommand.AddCommand(deployRemoveCommand) + deployRemoveCommand.Flags().StringVar(&flagEnvironment, "env", "", "The environment to remove e.g. staging or production") + deployRemoveCommand.Flags().StringVar(&flagDeployRuntimeBinary, "runtime-binary", "", "") + + deployCommand.AddCommand(deployLogsCommand) + deployLogsCommand.Flags().StringVar(&flagEnvironment, "env", "", "The environment to view logs for e.g. staging or production") + deployLogsCommand.Flags().DurationVar(&flagDeployLogsSince, "since", 0, "--since 1h") + deployLogsCommand.Flags().StringVar(&flagDeployLogsStart, "start", "", "--start '2024-11-01 09:00:00'") + + deployCommand.AddCommand(deploySecretsCommand) + + deploySecretsCommand.AddCommand(deploySecretsSetCommand) + deploySecretsSetCommand.Flags().StringVar(&flagEnvironment, "env", "", "The environment to set the secret for e.g. staging or production") + + deploySecretsCommand.AddCommand(deploySecretsGetCommand) + deploySecretsGetCommand.Flags().StringVar(&flagEnvironment, "env", "", "The environment to get the secret for e.g. staging or production") + + deploySecretsCommand.AddCommand(deploySecretsListCommand) + deploySecretsListCommand.Flags().StringVar(&flagEnvironment, "env", "", "The environment to get the secret for e.g. staging or production") + + deploySecretsCommand.AddCommand(deploySecretsDeleteCommand) + deploySecretsDeleteCommand.Flags().StringVar(&flagEnvironment, "env", "", "The environment to delete the secret from e.g. staging or production") +} diff --git a/cmd/init.go b/cmd/init.go index 137fb2c48..338ceea66 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -66,13 +66,22 @@ func panicHandler() { return } + err, ok := r.(error) + message := "Unknown error" + if ok { + message = err.Error() + } + errStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("15")). Background(lipgloss.Color("1")) fmt.Println("") fmt.Println(errStyle.Render("======= Oh no ==========")) - fmt.Println("Something seems to have gone wrong.") + fmt.Println("Something seems to have gone wrong:") + fmt.Println("") + fmt.Println(" >", message) + fmt.Println("") fmt.Println("This is likely a bug with Keel - please let us know via:") fmt.Println(" - Discord (https://discord.gg/HV8g38nBnm)") fmt.Println(" - GitHub Issue (https://github.com/teamkeel/keel/issues/new)") @@ -81,6 +90,8 @@ func panicHandler() { fmt.Println(colors.Gray(string(debug.Stack()))) fmt.Println(errStyle.Render("========================")) fmt.Println("") + + os.Exit(1) } } diff --git a/config/config.go b/config/config.go index f8a0c1dd6..11de5f1ce 100644 --- a/config/config.go +++ b/config/config.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "log" "os" "path/filepath" "regexp" @@ -38,6 +37,8 @@ type ProjectConfig struct { Secrets []Secret `yaml:"secrets"` Auth AuthConfig `yaml:"auth"` Console ConsoleConfig `yaml:"console"` + DisableAuth bool `yaml:"disableKeelAuth"` + Deploy *DeployConfig `yaml:"deploy,omitempty"` } func (p *ProjectConfig) GetEnvVars() map[string]string { @@ -243,7 +244,7 @@ func parseAndValidate(data []byte, filename string) (*ProjectConfig, error) { result, err := gojsonschema.Validate(schemaLoader, documentLoader) if err != nil { - log.Fatalf("Error validating JSON: %v", err) + return nil, err } errors := &ConfigErrors{} @@ -283,6 +284,7 @@ var validators = []ValidationFunc{ validateUniqueNames, validateReservedPrefixes, validateAuthProviders, + validateDatabase, } func validateReservedPrefixes(c *ProjectConfig) []*ConfigError { diff --git a/config/deploy.go b/config/deploy.go new file mode 100644 index 000000000..f9364d679 --- /dev/null +++ b/config/deploy.go @@ -0,0 +1,52 @@ +package config + +type DeployConfig struct { + ProjectName string `yaml:"projectName"` + Region string `yaml:"region"` + Database *DatabaseConfig `yaml:"database,omitempty"` + Jobs *JobsConfig `yaml:"jobs,omitempty"` + Telemetry *TelemetryConfig `yaml:"telemetry,omitempty"` +} + +type DatabaseConfig struct { + Provider string `yaml:"provider"` + RDS *RDSConfig `yaml:"rds,omitempty"` +} + +type RDSConfig struct { + Instance *string `yaml:"instance,omitempty"` + MultiAZ *bool `yaml:"multiAz,omitempty"` + Storage *int `yaml:"storage,omitempty"` +} + +type JobsConfig struct { + WebhookURL string `yaml:"webhookUrl,omitempty"` +} + +type TelemetryConfig struct { + Collector string `yaml:"collector,omitempty"` +} + +func validateDatabase(c *ProjectConfig) []*ConfigError { + errors := []*ConfigError{} + + if c.Deploy == nil { + return errors + } + + if c.Deploy.Database == nil { + return errors + } + + db := c.Deploy.Database + + if db.Provider != "rds" && db.RDS != nil { + errors = append(errors, &ConfigError{ + Message: "deploy.database.rds: can only be provided if deploy.database.provider is 'rds'", + Field: "deploy.database.rds", + Type: "invalid-property", + }) + } + + return errors +} diff --git a/config/fixtures/test_additional_properties_invalid.yaml b/config/fixtures/test_additional_properties_invalid.yaml new file mode 100644 index 000000000..b37685be8 --- /dev/null +++ b/config/fixtures/test_additional_properties_invalid.yaml @@ -0,0 +1,29 @@ +# (root): Additional property unknown is not allowed +# environment.0: Additional property unknown is not allowed +# auth: Additional property unknown is not allowed +# auth.tokens: Additional property unknown is not allowed +# deploy: Additional property unknown is not allowed +# deploy.database: Additional property unknown is not allowed +# deploy.jobs: Additional property unknown is not allowed +# deploy.telemetry: Additional property unknown is not allowed + +unknown: foo +environment: + - name: MY_ENV + value: foo + unknown: foo +auth: + tokens: + unknown: foo + unknown: foo +deploy: + unknown: foo + projectName: my-app + region: eu-west-2 + database: + provider: rds + unknown: foo + telemetry: + unknown: foo + jobs: + unknown: foo diff --git a/config/fixtures/test_deploy.yaml b/config/fixtures/test_deploy.yaml new file mode 100644 index 000000000..deddda496 --- /dev/null +++ b/config/fixtures/test_deploy.yaml @@ -0,0 +1,3 @@ +deploy: + projectName: my-project + region: us-east-2 diff --git a/config/fixtures/test_deploy_database_invalid.yaml b/config/fixtures/test_deploy_database_invalid.yaml new file mode 100644 index 000000000..169ac0610 --- /dev/null +++ b/config/fixtures/test_deploy_database_invalid.yaml @@ -0,0 +1,15 @@ +# deploy.database.provider: deploy.database.provider must be one of the following: "rds", "external" +# deploy.database.rds: can only be provided if deploy.database.provider is 'rds' +# deploy.database.rds.instance: Does not match format 'rds-instance-type' +# deploy.database.rds.storage: Must be greater than or equal to 20 +# deploy.database.rds: Additional property username is not allowed + +deploy: + projectName: my-project + region: us-east-2 + database: + provider: sqlite + rds: + instance: big + storage: 15 + username: foo diff --git a/config/fixtures/test_deploy_database_rds.yaml b/config/fixtures/test_deploy_database_rds.yaml new file mode 100644 index 000000000..fc6e74092 --- /dev/null +++ b/config/fixtures/test_deploy_database_rds.yaml @@ -0,0 +1,9 @@ +deploy: + projectName: my-project + region: us-east-2 + database: + provider: rds + rds: + storage: 50 + multiAz: true + instance: db.t4g.medium diff --git a/config/fixtures/test_deploy_database_rds_invalid.yaml b/config/fixtures/test_deploy_database_rds_invalid.yaml new file mode 100644 index 000000000..bee0473f6 --- /dev/null +++ b/config/fixtures/test_deploy_database_rds_invalid.yaml @@ -0,0 +1,9 @@ +# deploy.database.rds: can only be provided if deploy.database.provider is 'rds' + +deploy: + projectName: my-project + region: us-east-2 + database: + provider: external + rds: + instance: db.t4g.medium diff --git a/config/fixtures/test_deploy_invalid_project_name.yaml b/config/fixtures/test_deploy_invalid_project_name.yaml new file mode 100644 index 000000000..17c9f5bde --- /dev/null +++ b/config/fixtures/test_deploy_invalid_project_name.yaml @@ -0,0 +1,5 @@ +# deploy.projectName: Does not match pattern '^[a-z][a-z-]+$' + +deploy: + projectName: My Project + region: eu-west-2 diff --git a/config/fixtures/test_deploy_invalid_region.yaml b/config/fixtures/test_deploy_invalid_region.yaml new file mode 100644 index 000000000..aca415852 --- /dev/null +++ b/config/fixtures/test_deploy_invalid_region.yaml @@ -0,0 +1,5 @@ +# deploy.region: Does not match pattern '^[a-z]{2}-[a-z]+-\d{1}$' + +deploy: + projectName: my-project + region: Frankfurt diff --git a/config/fixtures/test_deploy_jobs.yaml b/config/fixtures/test_deploy_jobs.yaml new file mode 100644 index 000000000..d9a40d694 --- /dev/null +++ b/config/fixtures/test_deploy_jobs.yaml @@ -0,0 +1,5 @@ +deploy: + projectName: my-project + region: eu-west-2 + jobs: + webhookUrl: https://my-domain.com/webhooks/jobs diff --git a/config/fixtures/test_deploy_jobs_invalid.yaml b/config/fixtures/test_deploy_jobs_invalid.yaml new file mode 100644 index 000000000..6e6b7e4f6 --- /dev/null +++ b/config/fixtures/test_deploy_jobs_invalid.yaml @@ -0,0 +1,9 @@ +# deploy.jobs: Additional property foo is not allowed +# deploy.jobs.webhookUrl: Does not match format 'uri' + +deploy: + projectName: my-project + region: eu-west-2 + jobs: + foo: bar + webhookUrl: this-is-not-a-url diff --git a/config/fixtures/test_deploy_missing_project_name.yaml b/config/fixtures/test_deploy_missing_project_name.yaml new file mode 100644 index 000000000..62881bbde --- /dev/null +++ b/config/fixtures/test_deploy_missing_project_name.yaml @@ -0,0 +1,4 @@ +# deploy: projectName is required + +deploy: + region: eu-west-2 diff --git a/config/fixtures/test_deploy_missing_region.yaml b/config/fixtures/test_deploy_missing_region.yaml new file mode 100644 index 000000000..68819c8f6 --- /dev/null +++ b/config/fixtures/test_deploy_missing_region.yaml @@ -0,0 +1,4 @@ +# deploy: region is required + +deploy: + projectName: my-project diff --git a/config/fixtures/test_deploy_project_name_invalid.yaml b/config/fixtures/test_deploy_project_name_invalid.yaml new file mode 100644 index 000000000..85bfa8f90 --- /dev/null +++ b/config/fixtures/test_deploy_project_name_invalid.yaml @@ -0,0 +1,5 @@ +# deploy.projectName: Does not match pattern '^[a-z][a-z-]+$' + +deploy: + projectName: not valid + region: eu-west-2 diff --git a/config/fixtures/test_deploy_project_name_missing.yaml b/config/fixtures/test_deploy_project_name_missing.yaml new file mode 100644 index 000000000..62881bbde --- /dev/null +++ b/config/fixtures/test_deploy_project_name_missing.yaml @@ -0,0 +1,4 @@ +# deploy: projectName is required + +deploy: + region: eu-west-2 diff --git a/config/fixtures/test_deploy_region_invalid.yaml b/config/fixtures/test_deploy_region_invalid.yaml new file mode 100644 index 000000000..4b6db523f --- /dev/null +++ b/config/fixtures/test_deploy_region_invalid.yaml @@ -0,0 +1,5 @@ +# deploy.region: Does not match pattern '^[a-z]{2}-[a-z]+-\d{1}$' + +deploy: + projectName: my-project + region: the-moon diff --git a/config/fixtures/test_deploy_region_missing.yaml b/config/fixtures/test_deploy_region_missing.yaml new file mode 100644 index 000000000..68819c8f6 --- /dev/null +++ b/config/fixtures/test_deploy_region_missing.yaml @@ -0,0 +1,4 @@ +# deploy: region is required + +deploy: + projectName: my-project diff --git a/config/fixtures/test_deploy_telemetry.yaml b/config/fixtures/test_deploy_telemetry.yaml new file mode 100644 index 000000000..903fbdd29 --- /dev/null +++ b/config/fixtures/test_deploy_telemetry.yaml @@ -0,0 +1,5 @@ +deploy: + projectName: my-project + region: eu-west-2 + telemetry: + collector: ./path/to/my/collector.yaml diff --git a/config/fixtures/test_deploy_telemetry_empty.yaml b/config/fixtures/test_deploy_telemetry_empty.yaml new file mode 100644 index 000000000..bbbcfc881 --- /dev/null +++ b/config/fixtures/test_deploy_telemetry_empty.yaml @@ -0,0 +1,4 @@ +deploy: + projectName: my-project + region: eu-west-2 + telemetry: {} diff --git a/config/format.go b/config/format.go new file mode 100644 index 000000000..f26867b58 --- /dev/null +++ b/config/format.go @@ -0,0 +1,24 @@ +package config + +import ( + "regexp" + + "github.com/xeipuuv/gojsonschema" +) + +// https://aws.amazon.com/rds/instance-types/ +type RDSInstanceTypeFormat struct{} + +var rdsInstanceTypeFormatRegex = regexp.MustCompile(`^db\.[\w\d]+\.[\w\d]+$`) + +func (f RDSInstanceTypeFormat) IsFormat(input interface{}) bool { + v, ok := input.(string) + if !ok { + return false + } + return rdsInstanceTypeFormatRegex.MatchString(v) +} + +func init() { + gojsonschema.FormatCheckers.Add("rds-instance-type", RDSInstanceTypeFormat{}) +} diff --git a/config/schema.json b/config/schema.json index 4b4b2ad00..d40ac5204 100644 --- a/config/schema.json +++ b/config/schema.json @@ -1,8 +1,9 @@ { + "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "environment": { - "type": [ "array", "null" ], + "type": ["array", "null"], "items": { "type": "object", "properties": { @@ -20,7 +21,7 @@ } }, "secrets": { - "type": [ "array", "null" ], + "type": ["array", "null"], "items": { "type": "object", "properties": { @@ -38,7 +39,7 @@ "type": "boolean" }, "auth": { - "type": [ "object", "null" ], + "type": ["object", "null"], "properties": { "redirectUrl": { "type": "string", @@ -128,7 +129,7 @@ "additionalProperties": false }, "console": { - "type": [ "object", "null" ], + "type": ["object", "null"], "properties": { "api": { "type": "string" @@ -138,6 +139,71 @@ }, "disableKeelAuth": { "type": "boolean" + }, + "deploy": { + "type": "object", + "properties": { + "projectName": { + "type": "string", + "pattern": "^[a-z][a-z-]+$", + "description": "Project name in slug format. Only lower-case letters and dashes are allowed." + }, + "region": { + "type": "string", + "pattern": "^[a-z]{2}-[a-z]+-\\d{1}$", + "description": "The AWS region to deploy to" + }, + "database": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": ["rds", "external"] + }, + "rds": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "rds-instance-type" + }, + "multiAz": { + "type": "boolean" + }, + "storage": { + "type": "number", + "minimum": 20, + "maximum": 65536 + } + }, + "additionalProperties": false + } + }, + "required": ["provider"], + "additionalProperties": false + }, + "jobs": { + "type": "object", + "properties": { + "webhookUrl": { + "type": "string", + "format": "uri" + } + }, + "additionalProperties": false + }, + "telemetry": { + "type": "object", + "properties": { + "collector": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "required": ["projectName", "region"], + "additionalProperties": false } }, diff --git a/deploy/build.go b/deploy/build.go new file mode 100644 index 000000000..15f009218 --- /dev/null +++ b/deploy/build.go @@ -0,0 +1,426 @@ +package deploy + +import ( + "bytes" + "context" + _ "embed" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/aws/aws-sdk-go-v2/service/ssm/types" + "github.com/iancoleman/strcase" + "github.com/samber/lo" + "github.com/teamkeel/keel/codegen" + "github.com/teamkeel/keel/config" + "github.com/teamkeel/keel/node" + "github.com/teamkeel/keel/proto" + "github.com/teamkeel/keel/schema" + "google.golang.org/protobuf/encoding/protojson" + + esbuild "github.com/evanw/esbuild/pkg/api" +) + +//go:embed lambdas/functions/main.js.go.tmpl +var functionsHandlerTemplate string + +type BuildArgs struct { + ProjectRoot string + Env string + RuntimeBinary string + SkipRuntimeBinary bool + OnLoadSchema func(s *proto.Schema) *proto.Schema +} + +type BuildResult struct { + Schema *proto.Schema + SchemaPath string + Config *config.ProjectConfig + ConfigPath string + RuntimePath string + FunctionsBundledPath string + FunctionsPath string +} + +func Build(ctx context.Context, args *BuildArgs) (*BuildResult, error) { + heading("Build") + + t := NewTiming() + + projectConfig, err := loadKeelConfig(&LoadKeelConfigArgs{ + ProjectRoot: args.ProjectRoot, + Env: args.Env, + }) + if err != nil { + return nil, err + } + + builder := schema.Builder{} + protoSchema, err := builder.MakeFromDirectory(args.ProjectRoot) + if err != nil { + log("%s your Keel schema contains errors. Run `keel validate` to see details on these errors", IconCross) + return nil, err + } + if args.OnLoadSchema != nil { + protoSchema = args.OnLoadSchema(protoSchema) + } + + log("%s Found %s schema file(s) %s", IconTick, orange("%d", len(builder.SchemaFiles())), t.Since()) + + collectorConfig, err := buildCollectorConfig(ctx, &BuildCollectorConfigArgs{ + ProjectRoot: args.ProjectRoot, + Env: args.Env, + Config: projectConfig, + }) + if err != nil { + return nil, err + } + if collectorConfig != nil { + log("%s Using OpenTelemetry collector config", IconTick, orange("%d", len(builder.SchemaFiles())), t.Since()) + } + + runtimeResult, err := buildRuntime(ctx, &BuildRuntimeArgs{ + ProjectRoot: args.ProjectRoot, + Env: args.Env, + Schema: protoSchema, + Config: projectConfig, + RuntimeBinaryURL: args.RuntimeBinary, + SkipRuntimeBinaryDownload: args.SkipRuntimeBinary, + CollectorConfig: collectorConfig, + }) + if err != nil { + return nil, err + } + relPath, err := filepath.Rel(args.ProjectRoot, runtimeResult.Path) + if err != nil { + return nil, err + } + log("%s Built runtime into %s %s", IconTick, orange(relPath), t.Since()) + + functionsResult, err := buildFunctions(ctx, &BuildFunctionsArgs{ + ProjectRoot: args.ProjectRoot, + Schema: protoSchema, + Config: projectConfig, + CollectorConfig: collectorConfig, + }) + if err != nil { + return nil, err + } + + return &BuildResult{ + Schema: protoSchema, + SchemaPath: runtimeResult.SchemaPath, + Config: projectConfig, + ConfigPath: runtimeResult.ConfigPath, + RuntimePath: runtimeResult.Path, + FunctionsBundledPath: functionsResult.BundledPath, + FunctionsPath: functionsResult.Path, + }, nil +} + +type BuildRuntimeArgs struct { + ProjectRoot string + Env string + Schema *proto.Schema + Config *config.ProjectConfig + RuntimeBinaryURL string + SkipRuntimeBinaryDownload bool + CollectorConfig *string +} + +type BuildRuntimeResult struct { + SchemaPath string + ConfigPath string + Path string +} + +func buildRuntime( + ctx context.Context, // nolint: unparam + args *BuildRuntimeArgs, +) (*BuildRuntimeResult, error) { + buildDir := filepath.Join(args.ProjectRoot, ".build") + schemaPath := filepath.Join(buildDir, "runtime/schema.json") + configPath := filepath.Join(buildDir, "runtime/config.json") + + err := os.MkdirAll(filepath.Join(buildDir, "runtime"), os.ModePerm) + if err != nil { + log("%s error creating .build/runtime directory: %s", err.Error()) + return nil, err + } + + schemaJson, err := protojson.Marshal(args.Schema) + if err != nil { + log("%s error marshalling Keel schema to JSON", IconCross, err.Error()) + return nil, err + } + err = os.WriteFile(schemaPath, schemaJson, os.ModePerm) + if err != nil { + log("%s error writing schema JSON to build directory: %s", IconCross, err.Error()) + return nil, err + } + + configJSON, err := json.Marshal(args.Config) + if err != nil { + log("%s error marshalling Keel config to JSON", IconCross, err.Error()) + return nil, err + } + err = os.WriteFile(configPath, configJSON, os.ModePerm) + if err != nil { + log("%s error writing schema JSON to build directory: %s", IconCross, err.Error()) + return nil, err + } + + if !args.SkipRuntimeBinaryDownload { + var b []byte + if strings.HasPrefix(args.RuntimeBinaryURL, "http") { + t := NewTiming() + res, err := http.Get(args.RuntimeBinaryURL) + if err != nil { + log("%s error requesting %s: %s", IconCross, args.RuntimeBinaryURL, err.Error()) + return nil, err + } + if res.StatusCode >= 300 { + return nil, fmt.Errorf("non-200 (%d) trying to fetch runtime binary", res.StatusCode) + } + b, err = io.ReadAll(res.Body) + if err != nil { + log("%s reading response from %s: %s", IconCross, args.RuntimeBinaryURL, err.Error()) + return nil, err + } + log("%s Fetched runtime binary from %s %s", IconTick, orange(args.RuntimeBinaryURL), t.Since()) + } else { + p := args.RuntimeBinaryURL + if !filepath.IsAbs(p) { + p = filepath.Join(args.ProjectRoot, p) + } + b, err = os.ReadFile(p) + if err != nil { + log("%s error reading local runtime binary %s: %s", IconCross, p, err.Error()) + return nil, err + } + } + + err = os.WriteFile(filepath.Join(buildDir, "runtime/bootstrap"), b, os.ModePerm) + if err != nil { + log("%s error writing runtime binary to build directory: %s", IconCross, err.Error()) + return nil, err + } + } + + if args.CollectorConfig != nil { + err = os.WriteFile(filepath.Join(buildDir, "runtime/collector.yaml"), []byte(*args.CollectorConfig), os.ModePerm) + if err != nil { + log("%s error writing collector config to build directory: %s", IconCross, err.Error()) + return nil, err + } + } + + return &BuildRuntimeResult{ + SchemaPath: schemaPath, + ConfigPath: configPath, + Path: filepath.Join(buildDir, "runtime"), + }, nil +} + +type BuildFunctionsArgs struct { + ProjectRoot string + Schema *proto.Schema + Config *config.ProjectConfig + CollectorConfig *string +} + +type BuildFunctionsResult struct { + BundledPath string + Path string +} + +func buildFunctions(ctx context.Context, args *BuildFunctionsArgs) (*BuildFunctionsResult, error) { + buildDir := filepath.Join(args.ProjectRoot, ".build") + + err := os.MkdirAll(filepath.Join(buildDir, "functions"), os.ModePerm) + if err != nil { + log("%s error creating .build/functions directory: %s", err.Error()) + return nil, err + } + + sdk, err := node.Generate(ctx, args.Schema, args.Config) + if err != nil { + return nil, err + } + + functionsHandler, err := generateFunctionsHandler(args.Schema, args.Config) + if err != nil { + return nil, err + } + + sdk = append(sdk, functionsHandler) + + err = sdk.Write(args.ProjectRoot) + if err != nil { + return nil, err + } + + deploy := args.Config.Deploy + if deploy != nil && (deploy.Database == nil || deploy.Database.Provider == "rds") { + t := NewTiming() + res, err := http.Get("https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem") + if err != nil { + return nil, err + } + if res.StatusCode >= 300 { + return nil, fmt.Errorf("non-200 (%d) fetching .pem for RDS", res.StatusCode) + } + b, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + err = os.WriteFile(filepath.Join(buildDir, "functions/rds.pem"), b, os.ModePerm) + if err != nil { + return nil, err + } + log("%s Downloaded RDS public key %s", IconTick, t.Since()) + } + + if args.CollectorConfig != nil { + err = os.WriteFile(filepath.Join(buildDir, "functions/collector.yaml"), []byte(*args.CollectorConfig), os.ModePerm) + if err != nil { + return nil, err + } + } + + bundledPath := filepath.Join(buildDir, "functions/main-bundled.js") + + t := NewTiming() + res := esbuild.Build(esbuild.BuildOptions{ + EntryPoints: []string{filepath.Join(buildDir, "functions/main.js")}, + Outfile: bundledPath, + Bundle: true, + Write: true, + AllowOverwrite: true, + Target: esbuild.ESNext, + Platform: esbuild.PlatformNode, + Format: esbuild.FormatCommonJS, + Sourcemap: esbuild.SourceMapLinked, + External: []string{"pg-native"}, + Loader: map[string]esbuild.Loader{ + ".node": esbuild.LoaderFile, + }, + MinifyWhitespace: true, + MinifyIdentifiers: true, + MinifySyntax: true, + KeepNames: true, + }) + if len(res.Errors) > 0 { + return nil, fmt.Errorf("esbuild error: %s", res.Errors[0].Text) + } + + rel, _ := filepath.Rel(args.ProjectRoot, filepath.Join(buildDir, "functions")) + log("%s Built functions into %s %s", IconTick, orange(rel), t.Since()) + + return &BuildFunctionsResult{ + BundledPath: bundledPath, + Path: filepath.Join(buildDir, "functions"), + }, nil +} + +type BuildCollectorConfigArgs struct { + ProjectRoot string + Env string + Config *config.ProjectConfig +} + +func buildCollectorConfig(ctx context.Context, args *BuildCollectorConfigArgs) (*string, error) { + deploy := args.Config.Deploy + if deploy == nil { + return nil, nil + } + + telemetry := deploy.Telemetry + if telemetry == nil || telemetry.Collector == "" { + return nil, nil + } + + collectorPath := filepath.Join(args.ProjectRoot, telemetry.Collector) + + params, err := ListSecrets(ctx, &ListSecretsArgs{ + ProjectRoot: args.ProjectRoot, + Env: args.Env, + }) + if err != nil { + return nil, err + } + + secrets := lo.Reduce(params, func(m map[string]string, p types.Parameter, _ int) map[string]string { + m[*p.Name] = *p.Value + return m + }, map[string]string{}) + + t := template.Must(template.New("collector").ParseFiles(collectorPath)) + b := bytes.Buffer{} + err = t.Execute(&b, map[string]any{ + "secrets": secrets, + }) + if err != nil { + log("%s error rendering secrets in OTEL collector config: %s", IconCross, err.Error()) + return nil, err + } + + result := b.String() + return &result, nil +} + +func generateFunctionsHandler(schema *proto.Schema, cfg *config.ProjectConfig) (*codegen.GeneratedFile, error) { + functions := map[string]string{} + jobs := []string{} + subscribers := []string{} + actionTypes := map[string]string{} + + for _, model := range schema.Models { + for _, op := range model.Actions { + if op.Implementation != proto.ActionImplementation_ACTION_IMPLEMENTATION_CUSTOM { + continue + } + functions[op.Name] = op.Name + actionTypes[op.Name] = op.Type.String() + } + } + + if cfg != nil { + for _, v := range cfg.Auth.EnabledHooks() { + functions[string(v)] = fmt.Sprintf("auth/%s", string(v)) + } + } + + for _, job := range schema.Jobs { + jobName := strcase.ToLowerCamel(job.Name) + jobs = append(jobs, jobName) + } + + for _, subscriber := range schema.Subscribers { + subscriberName := strcase.ToLowerCamel(subscriber.Name) + subscribers = append(subscribers, subscriberName) + } + + var tmpl = template.Must(template.New("handler.js").Parse(functionsHandlerTemplate)) + + b := bytes.Buffer{} + err := tmpl.Execute(&b, map[string]interface{}{ + "Functions": functions, + "Subscribers": subscribers, + "Jobs": jobs, + "ActionTypes": actionTypes, + }) + if err != nil { + return nil, err + } + + return &codegen.GeneratedFile{ + Path: ".build/functions/main.js", + Contents: b.String(), + }, nil +} diff --git a/deploy/database.go b/deploy/database.go new file mode 100644 index 000000000..ba6769612 --- /dev/null +++ b/deploy/database.go @@ -0,0 +1,41 @@ +package deploy + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" +) + +type GetRDSConnectionArgs struct { + Cfg aws.Config + Endpoint string + DbName string + SecretArn string +} + +func GetRDSConnection(ctx context.Context, args *GetRDSConnectionArgs) (string, error) { + r, err := secretsmanager.NewFromConfig(args.Cfg).GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{ + SecretId: aws.String(args.SecretArn), + }) + if err != nil { + return "", err + } + + type Secret struct { + Username string `json:"username"` + Password string `json:"password"` + } + + var secret Secret + err = json.Unmarshal([]byte(*r.SecretString), &secret) + if err != nil { + return "", err + } + + encodedPassword := url.QueryEscape(secret.Password) + return fmt.Sprintf("postgres://%s:%s@%s/%s", secret.Username, encodedPassword, args.Endpoint, args.DbName), nil +} diff --git a/deploy/lambdas/functions/main.js.go.tmpl b/deploy/lambdas/functions/main.js.go.tmpl new file mode 100644 index 000000000..2e87d994e --- /dev/null +++ b/deploy/lambdas/functions/main.js.go.tmpl @@ -0,0 +1,85 @@ +const process = require("node:process"); +const { + handleRequest, + handleJob, + handleSubscriber, + tracing, +} = require("@teamkeel/functions-runtime"); +const { + createContextAPI, + createJobContextAPI, + createSubscriberContextAPI, + permissionFns, +} = require("@teamkeel/sdk"); + +const functions = { +{{ range $name, $path := .Functions }} + {{ $name }}: require("../../functions/{{ $path }}").default, +{{ end }} +}; + +const subscribers = { +{{ range .Subscribers }} + {{ . }}: require("../../subscribers/{{ . }}").default, +{{ end }} +}; + +const jobs = { +{{ range .Jobs }} + {{ . }}: require("../../jobs/{{ . }}").default, +{{ end }} +}; + +const actionTypes = { +{{ range $name, $type := .ActionTypes }} + {{ $name }}: "{{ $type }}", +{{ end }} +}; + +export async function handler(event) { + if (event.rawPath === "/_health") { + return { + id: "ok", + result: {}, + }; + } + + let rpcResponse = null; + + try { + switch (event.type) { + case "action": + rpcResponse = await handleRequest(event, { + functions, + createContextAPI, + actionTypes, + permissionFns, + }); + break; + case "job": + rpcResponse = await handleJob(event, { + jobs, + createJobContextAPI, + }); + break; + case "subscriber": + rpcResponse = await handleSubscriber(event, { + subscribers, + createSubscriberContextAPI, + }); + break; + } + } catch (e) { + console.error("unexpected handler error", e); + } finally { + await tracing.forceFlush(); + } + + return rpcResponse; +} + +tracing.init(); + +process.on("unhandledRejection", (reason, promise) => { + console.error("unhandled promise rejection", promise, "reason:", reason); +}); diff --git a/deploy/lambdas/runtime/api.go b/deploy/lambdas/runtime/api.go new file mode 100644 index 000000000..2d3a83f17 --- /dev/null +++ b/deploy/lambdas/runtime/api.go @@ -0,0 +1,134 @@ +package runtime + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/aws/aws-lambda-go/events" + "github.com/sirupsen/logrus" + "github.com/teamkeel/keel/runtime" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +// ResponseWriter is a minimal implementation of http.ResponseWriter that +// simply stores the response for later inspection. +type ResponseWriter struct { + StatusCode int + Body bytes.Buffer + HeaderMap http.Header +} + +// Header needed to implement http.ResponseWriter. +func (c *ResponseWriter) Header() http.Header { + return c.HeaderMap +} + +// Write needed to implement http.ResponseWriter. +func (c *ResponseWriter) Write(b []byte) (int, error) { + return c.Body.Write(b) +} + +// WriteHeader needed to implement http.ResponseWriter. +func (c *ResponseWriter) WriteHeader(statusCode int) { + c.StatusCode = statusCode +} + +func (h *Handler) APIHandler(ctx context.Context, request events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) { + defer func() { + if h.tracerProvider != nil { + h.tracerProvider.ForceFlush(ctx) + } + }() + + ctx, span := h.tracer.Start(ctx, fmt.Sprintf("%s %s", request.RequestContext.HTTP.Method, request.RequestContext.HTTP.Path)) + defer span.End() + + h.log.WithFields(logrus.Fields{ + "method": request.RequestContext.HTTP.Method, + "path": request.RequestContext.HTTP.Path, + }).Info("API request") + + span.SetAttributes( + attribute.String("type", "request"), + attribute.String("http.method", request.RequestContext.HTTP.Method), + attribute.String("http.path", request.RequestContext.HTTP.Path), + attribute.String("http.useragent", request.RequestContext.HTTP.UserAgent), + attribute.String("http.ipAddress", request.RequestContext.HTTP.SourceIP), + attribute.String("aws.requestID", request.RequestContext.RequestID), + ) + + if request.RawPath == "/_health" { + statusResponse, _ := json.Marshal(map[string]string{"status": "ok"}) + return events.LambdaFunctionURLResponse{ + StatusCode: http.StatusOK, + Body: string(statusResponse), + }, nil + } + + ctx, err := h.buildContext(ctx) + if err != nil { + span.RecordError(err, trace.WithStackTrace(true)) + span.SetStatus(codes.Error, err.Error()) + return events.LambdaFunctionURLResponse{ + StatusCode: http.StatusInternalServerError, + }, nil + } + + handler := runtime.NewHttpHandler(h.schema) + + headers := http.Header{} + for k, v := range request.Headers { + headers.Set(k, v) + } + + body := request.Body + if request.IsBase64Encoded { + decoded, err := base64.StdEncoding.DecodeString(body) + if err != nil { + span.RecordError(err, trace.WithStackTrace(true)) + span.SetStatus(codes.Error, err.Error()) + return events.LambdaFunctionURLResponse{ + StatusCode: http.StatusInternalServerError, + }, nil + } + body = string(decoded) + } + + runtimeRequest := &http.Request{ + Method: request.RequestContext.HTTP.Method, + URL: &url.URL{ + Path: request.RequestContext.HTTP.Path, + RawQuery: request.RawQueryString, + }, + Header: headers, + Body: io.NopCloser(strings.NewReader(body)), + } + + runtimeRequest = runtimeRequest.WithContext(ctx) + + crw := &ResponseWriter{ + HeaderMap: make(http.Header), + } + + handler.ServeHTTP(crw, runtimeRequest) + + responseHeaders := map[string]string{} + for k, v := range crw.HeaderMap { + responseHeaders[k] = v[0] + } + + return events.LambdaFunctionURLResponse{ + StatusCode: crw.StatusCode, + Body: crw.Body.String(), + Headers: responseHeaders, + }, nil +} diff --git a/deploy/lambdas/runtime/cmd/main.go b/deploy/lambdas/runtime/cmd/main.go new file mode 100644 index 000000000..d59934192 --- /dev/null +++ b/deploy/lambdas/runtime/cmd/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "os" + "strings" + + "github.com/aws/aws-lambda-go/lambda" + "github.com/teamkeel/keel/deploy/lambdas/runtime" +) + +func main() { + h, err := runtime.New(context.Background(), &runtime.HandlerArgs{ + LogLevel: os.Getenv("KEEL_LOG_LEVEL"), + SchemaPath: "/var/task/schema.json", + ConfigPath: "/var/task/config.json", + ProjectName: os.Getenv("KEEL_PROJECT_NAME"), + Env: os.Getenv("KEEL_ENV"), + QueueURL: os.Getenv("KEEL_QUEUE_URL"), + FunctionsARN: os.Getenv("KEEL_FUNCTIONS_ARN"), + BucketName: os.Getenv("KEEL_FILES_BUCKET_NAME"), + SecretNames: strings.Split(os.Getenv("KEEL_SECRET_NAMES"), ":"), + JobsWebhookURL: os.Getenv("KEEL_JOBS_WEBHOOK_URL"), + DBEndpoint: os.Getenv("KEEL_DATABASE_ENDPOINT"), + DBName: os.Getenv("KEEL_DATABASE_DB_NAME"), + DBSecretArn: os.Getenv("KEEL_DATABASE_SECRET_ARN"), + }) + if err != nil { + panic(err) + } + + switch os.Getenv("KEEL_RUNTIME_MODE") { + case runtime.RuntimeModeApi: + lambda.Start(h.APIHandler) + case runtime.RuntimeModeSubscriber: + lambda.Start(h.EventHandler) + case runtime.RuntimeModeJob: + lambda.Start(h.JobHandler) + } +} diff --git a/deploy/lambdas/runtime/db.go b/deploy/lambdas/runtime/db.go new file mode 100644 index 000000000..1fbe631c0 --- /dev/null +++ b/deploy/lambdas/runtime/db.go @@ -0,0 +1,79 @@ +package runtime + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + "github.com/teamkeel/keel/db" +) + +func initDB(secrets map[string]string, dbEndpoint, dbName, secretName string) (db.Database, error) { + ctx := context.Background() + + dbConnString := "" + + defer func() { + // Need to add this to secrets for functions-runtime + secrets["KEEL_DB_CONN"] = dbConnString + }() + + // Try and get url from secrets (external database) + var ok bool + dbConnString, ok = secrets["DATABASE_URL"] + if ok { + return db.New(ctx, dbConnString) + } + + // Otherwise fallback to secrets manager (RDS) + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + panic(err) + } + + dbConnString, err = GetRDSConnection(ctx, &GetRDSConnectionArgs{ + Cfg: cfg, + Endpoint: dbEndpoint, + DbName: dbName, + SecretArn: secretName, + }) + if err != nil { + panic(err) + } + + return db.New(ctx, dbConnString) +} + +type GetRDSConnectionArgs struct { + Cfg aws.Config + Endpoint string + DbName string + SecretArn string +} + +func GetRDSConnection(ctx context.Context, args *GetRDSConnectionArgs) (string, error) { + r, err := secretsmanager.NewFromConfig(args.Cfg).GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{ + SecretId: aws.String(args.SecretArn), + }) + if err != nil { + return "", err + } + + type Secret struct { + Username string `json:"username"` + Password string `json:"password"` + } + + var secret Secret + err = json.Unmarshal([]byte(*r.SecretString), &secret) + if err != nil { + return "", err + } + + encodedPassword := url.QueryEscape(secret.Password) + return fmt.Sprintf("postgres://%s:%s@%s/%s", secret.Username, encodedPassword, args.Endpoint, args.DbName), nil +} diff --git a/deploy/lambdas/runtime/events.go b/deploy/lambdas/runtime/events.go new file mode 100644 index 000000000..7d1bcb3e4 --- /dev/null +++ b/deploy/lambdas/runtime/events.go @@ -0,0 +1,125 @@ +package runtime + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + lambdaevents "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sqs" + "github.com/sirupsen/logrus" + "github.com/teamkeel/keel/events" + "github.com/teamkeel/keel/runtime" + "github.com/teamkeel/keel/util" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +type EventPayload struct { + Subscriber string `json:"subscriber,omitempty"` + Event *events.Event `json:"event,omitempty"` + Traceparent string `json:"traceparent,omitempty"` +} + +func initEvents(queueUrl string, awsEndpoint string) (events.EventHandler, error) { + cfg, err := config.LoadDefaultConfig(context.Background()) + if err != nil { + return nil, err + } + + opts := []func(*sqs.Options){} + if awsEndpoint != "" { + opts = append(opts, func(o *sqs.Options) { + o.BaseEndpoint = &awsEndpoint + }) + } + + client := sqs.NewFromConfig(cfg, opts...) + + return func(ctx context.Context, subscriber string, event *events.Event, traceparent string) error { + return sendEvent(ctx, client, queueUrl, subscriber, event, traceparent) + }, nil +} + +func sendEvent(ctx context.Context, client *sqs.Client, queueURL string, subscriber string, event *events.Event, traceparent string) error { + payload := EventPayload{ + Subscriber: subscriber, + Event: event, + Traceparent: traceparent, + } + + bodyBytes, err := json.Marshal(payload) + if err != nil { + return err + } + + input := &sqs.SendMessageInput{ + MessageBody: aws.String(string(bodyBytes)), + QueueUrl: aws.String(queueURL), + } + + _, err = client.SendMessage(ctx, input) + return err +} + +func (h *Handler) EventHandler(ctx context.Context, event lambdaevents.SQSEvent) error { + defer func() { + if h.tracerProvider != nil { + h.tracerProvider.ForceFlush(ctx) + } + }() + + if len(event.Records) != 1 { + return fmt.Errorf("event lambda is only designed to process exactly one message at a time, received %v", len(event.Records)) + } + + message := event.Records[0] + + var payload EventPayload + err := json.Unmarshal([]byte(message.Body), &payload) + if err != nil { + return err + } + + if payload.Event == nil { + return errors.New("event is nil") + } + + h.log.WithFields(logrus.Fields{ + "subscriber": payload.Subscriber, + "eventName": payload.Event.EventName, + "type": payload.Event.Target.Type, + "id": payload.Event.Target.Id, + }).Info("Event received") + + // Use the span context from the event payload, which + // originates from the runtime execution that triggered the event. + spanContext := util.ParseTraceparent(payload.Traceparent) + if spanContext.IsValid() { + ctx = trace.ContextWithSpanContext(ctx, spanContext) + } + + ctx, span := h.tracer.Start(ctx, "Process event") + defer span.End() + + span.SetAttributes( + attribute.String("type", "event"), + attribute.String("event.name", payload.Event.EventName), + attribute.String("event.messageId", message.MessageId), + attribute.String("subscriber.name", payload.Subscriber), + ) + + ctx, err = h.buildContext(ctx) + if err != nil { + span.RecordError(err, trace.WithStackTrace(true)) + span.SetStatus(codes.Error, err.Error()) + return err + } + + err = runtime.NewSubscriberHandler(h.schema).RunSubscriber(ctx, payload.Subscriber, payload.Event) + return err +} diff --git a/deploy/lambdas/runtime/files.go b/deploy/lambdas/runtime/files.go new file mode 100644 index 000000000..a2270f1a0 --- /dev/null +++ b/deploy/lambdas/runtime/files.go @@ -0,0 +1,176 @@ +package runtime + +import ( + "bytes" + "context" + "errors" + "fmt" + "net/url" + "path" + "time" + + "github.com/segmentio/ksuid" + "github.com/vincent-petithory/dataurl" + "go.opentelemetry.io/otel/trace" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/s3" + endpoints "github.com/aws/smithy-go/endpoints" + "github.com/teamkeel/keel/storage" +) + +const ( + FileObjectExpiryDuration = time.Hour + FileObjectPrefix = "files/" +) + +var _ storage.Storer = &S3BucketStore{} + +type S3BucketStore struct { + // TODO: this is an anti-pattern - the methods in storage.Storer should all take context instead + context context.Context + + client *s3.Client + bucketName string + tracer trace.Tracer +} + +// If a custom endpoint is set we need to use a custom resolver. Just settng the base endpoint isn't enough for S3 as it +// as the default resolver uses the bucket name as a sub-domain, which likely won't work with the custom endpoint. +// By impleenting a full resolver we can force it to be the endpoint we want. +type CustomS3EndpointResolverV2 struct { + endpoint string +} + +func (e *CustomS3EndpointResolverV2) ResolveEndpoint(ctx context.Context, params s3.EndpointParameters) (endpoints.Endpoint, error) { + v, err := url.Parse(e.endpoint) + if err != nil { + return endpoints.Endpoint{}, err + } + + return endpoints.Endpoint{ + URI: *v, + }, nil +} + +func initFiles(ctx context.Context, tracer trace.Tracer, bucketName, awsEndpoint string) (*S3BucketStore, error) { + cfg, err := config.LoadDefaultConfig(context.Background()) + if err != nil { + return nil, err + } + + opts := []func(*s3.Options){} + if awsEndpoint != "" { + opts = append(opts, s3.WithEndpointResolverV2(&CustomS3EndpointResolverV2{ + endpoint: awsEndpoint, + })) + } + + client := s3.NewFromConfig(cfg, opts...) + + return &S3BucketStore{ + context: ctx, + tracer: tracer, + client: client, + bucketName: bucketName, + }, nil +} + +func (s S3BucketStore) GetFileInfo(key string) (storage.FileInfo, error) { + if s.bucketName == "" { + return storage.FileInfo{}, errors.New("S3 bucket name cannot be empty") + } + + pathedKey := path.Join(FileObjectPrefix, key) + + object, err := s.client.GetObject(s.context, &s3.GetObjectInput{ + Bucket: &s.bucketName, + Key: &pathedKey}) + if err != nil { + return storage.FileInfo{}, err + } + + return storage.FileInfo{ + Key: key, + Filename: object.Metadata["filename"], + ContentType: *object.ContentType, + Size: int(*object.ContentLength), + }, nil +} + +func (s S3BucketStore) Store(dataURL string) (storage.FileInfo, error) { + var span trace.Span + s.context, span = s.tracer.Start(s.context, "Store File") + defer span.End() + + if s.bucketName == "" { + return storage.FileInfo{}, errors.New("S3 bucket name cannot be empty") + } + + durl, err := dataurl.DecodeString(dataURL) + if err != nil { + return storage.FileInfo{}, fmt.Errorf("decoding dataurl: %w", err) + } + + key := ksuid.New().String() + pathedKey := path.Join(FileObjectPrefix, key) + ct := durl.ContentType() + + _, err = s.client.PutObject(s.context, &s3.PutObjectInput{ + Bucket: &s.bucketName, + Key: &pathedKey, + Body: bytes.NewReader(durl.Data), + ContentType: &ct, + Metadata: map[string]string{"filename": durl.Params["name"]}}) + if err != nil { + return storage.FileInfo{}, fmt.Errorf("storing file: %w", err) + } + + return s.GetFileInfo(key) +} + +func (s S3BucketStore) GenerateFileResponse(fi *storage.FileInfo) (storage.FileResponse, error) { + var span trace.Span + s.context, span = s.tracer.Start(s.context, "Hydrate File") + defer span.End() + + if s.bucketName == "" { + return storage.FileResponse{}, errors.New("S3 bucket name cannot be empty") + } + + hydrated, err := s.getPresignedURL(fi) + if err != nil { + return storage.FileResponse{}, fmt.Errorf("hydrating file info: %w", err) + } + + return hydrated, nil +} + +func (s S3BucketStore) getPresignedURL(fi *storage.FileInfo) (storage.FileResponse, error) { + if s.bucketName == "" { + return storage.FileResponse{}, errors.New("S3 bucket name cannot be empty") + } + + pathedKey := path.Join(FileObjectPrefix, fi.Key) + + presignClient := s3.NewPresignClient(s.client) + request, err := presignClient.PresignGetObject(s.context, &s3.GetObjectInput{ + Bucket: &s.bucketName, + Key: &pathedKey, + ResponseContentDisposition: aws.String(fmt.Sprintf(`attachment; filename="%s"`, fi.Filename)), + }, func(opts *s3.PresignOptions) { + opts.Expires = FileObjectExpiryDuration + }) + if err != nil { + return storage.FileResponse{}, fmt.Errorf("couldn't get a presigned url for %s:%s. %w", s.bucketName, pathedKey, err) + } + + return storage.FileResponse{ + Key: fi.Key, + Filename: fi.Filename, + ContentType: fi.ContentType, + Size: fi.Size, + URL: request.URL, + }, nil +} diff --git a/deploy/lambdas/runtime/functions.go b/deploy/lambdas/runtime/functions.go new file mode 100644 index 000000000..1785f7858 --- /dev/null +++ b/deploy/lambdas/runtime/functions.go @@ -0,0 +1,108 @@ +package runtime + +import ( + "fmt" + + "context" + "encoding/json" + + "github.com/teamkeel/keel/functions" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/lambda" + lambdaTypes "github.com/aws/aws-sdk-go-v2/service/lambda/types" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +func initFunctions(ctx context.Context, functionsArn string, awsEndpoint string) (functions.Transport, error) { + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return nil, err + } + + opts := []func(*lambda.Options){} + if awsEndpoint != "" { + opts = append(opts, func(o *lambda.Options) { + o.BaseEndpoint = &awsEndpoint + }) + } + + client := lambda.NewFromConfig(cfg, opts...) + + return func(ctx context.Context, request *functions.FunctionsRuntimeRequest) (*functions.FunctionsRuntimeResponse, error) { + return invokeFunction(ctx, client, functionsArn, request) + }, nil +} + +type LambdaErrorResponse struct { + ErrorType string `json:"errorType,omitempty"` + ErrorMessage string `json:"errorMessage,omitempty"` + Trace []string `json:"stackTrace,omitempty"` +} + +func invokeFunction(ctx context.Context, client *lambda.Client, functionArn string, request *functions.FunctionsRuntimeRequest) (*functions.FunctionsRuntimeResponse, error) { + span := trace.SpanFromContext(ctx) + + requestJson, err := json.Marshal(request) + if err != nil { + return nil, err + } + + invokeOutput, err := client.Invoke(ctx, &lambda.InvokeInput{ + FunctionName: &functionArn, + InvocationType: lambdaTypes.InvocationTypeRequestResponse, + LogType: lambdaTypes.LogTypeNone, + Payload: requestJson, + }) + if err != nil { + return nil, err + } + + if invokeOutput.StatusCode < 200 || invokeOutput.StatusCode >= 300 { + err = fmt.Errorf("non-200 (%d) status from function", invokeOutput.StatusCode) + span.SetStatus(codes.Error, err.Error()) + span.SetAttributes(attribute.Int("function.response_status", int(invokeOutput.StatusCode))) + + if invokeOutput.FunctionError != nil { + span.SetAttributes(attribute.String("function.response_error", *invokeOutput.FunctionError)) + } + + if len(invokeOutput.Payload) > 0 { + span.SetAttributes(attribute.String("function.response_payload", string(invokeOutput.Payload))) + } + + return nil, err + } + + response := functions.FunctionsRuntimeResponse{ + Result: nil, + Error: nil, + } + + if invokeOutput.FunctionError != nil { + var lambdaResponse LambdaErrorResponse + + err = json.Unmarshal(invokeOutput.Payload, &lambdaResponse) + if err != nil { + return nil, err + } + + response = functions.FunctionsRuntimeResponse{ + Error: &functions.FunctionsRuntimeError{ + Message: lambdaResponse.ErrorMessage, + Code: functions.UnknownError, + }, + } + } + + if invokeOutput.Payload != nil { + err = json.Unmarshal(invokeOutput.Payload, &response) + if err != nil { + return nil, err + } + } + + return &response, nil +} diff --git a/deploy/lambdas/runtime/handler.go b/deploy/lambdas/runtime/handler.go new file mode 100644 index 000000000..29f03e62a --- /dev/null +++ b/deploy/lambdas/runtime/handler.go @@ -0,0 +1,220 @@ +package runtime + +import ( + "context" + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "os" + + "github.com/sirupsen/logrus" + "github.com/teamkeel/keel/config" + + "github.com/teamkeel/keel/db" + "github.com/teamkeel/keel/events" + "github.com/teamkeel/keel/functions" + "github.com/teamkeel/keel/proto" + "github.com/teamkeel/keel/runtime/runtimectx" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/encoding/protojson" +) + +const ( + RuntimeModeApi = "api" + RuntimeModeSubscriber = "subscriber" + RuntimeModeJob = "job" +) + +type Handler struct { + args *HandlerArgs + log *logrus.Logger + schema *proto.Schema + config *config.ProjectConfig + secrets map[string]string + privateKey *rsa.PrivateKey + db db.Database + functionsTransport functions.Transport + sqsEventHandler events.EventHandler + filesStorage *S3BucketStore + tracer trace.Tracer + tracerProvider *sdktrace.TracerProvider +} + +type HandlerArgs struct { + LogLevel string + SchemaPath string + ConfigPath string + ProjectName string + Env string + QueueURL string + FunctionsARN string + BucketName string + SecretNames []string + JobsWebhookURL string + + // For RDS + DBEndpoint string + DBName string + DBSecretArn string + + // For testing + AWSEndpoint string +} + +func New(ctx context.Context, args *HandlerArgs) (*Handler, error) { + tracer, tracerProvider := initTracing() + + s, err := initSchema(args.SchemaPath) + if err != nil { + return nil, err + } + + c, err := initConfig(args.ConfigPath) + if err != nil { + return nil, err + } + + secrets, err := initSecrets(ctx, args.SecretNames, args.AWSEndpoint) + if err != nil { + return nil, err + } + + pk, err := initPrivateKey(secrets) + if err != nil { + return nil, err + } + + db, err := initDB(secrets, args.DBEndpoint, args.DBName, args.DBSecretArn) + if err != nil { + return nil, err + } + + eventHandler, err := initEvents(args.QueueURL, args.AWSEndpoint) + if err != nil { + return nil, err + } + + files, err := initFiles(ctx, tracer, args.BucketName, args.AWSEndpoint) + if err != nil { + return nil, err + } + + log, err := initLogger(args.LogLevel) + if err != nil { + return nil, err + } + + functionsTransport, err := initFunctions(ctx, args.FunctionsARN, args.AWSEndpoint) + if err != nil { + return nil, err + } + + h := &Handler{ + args: args, + log: log, + schema: s, + config: c, + privateKey: pk, + secrets: secrets, + db: db, + functionsTransport: functionsTransport, + sqsEventHandler: eventHandler, + filesStorage: files, + tracer: tracer, + tracerProvider: tracerProvider, + } + + return h, nil +} + +func (h *Handler) Stop() error { + return h.db.Close() +} + +func initSchema(path string) (*proto.Schema, error) { + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + var s proto.Schema + err = protojson.Unmarshal(b, &s) + if err != nil { + return nil, err + } + + return &s, nil +} + +func initConfig(path string) (*config.ProjectConfig, error) { + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + var c config.ProjectConfig + err = json.Unmarshal(b, &c) + if err != nil { + return nil, err + } + + return &c, nil +} + +func initPrivateKey(secrets map[string]string) (*rsa.PrivateKey, error) { + privateKeyPem, ok := secrets["KEEL_PRIVATE_KEY"] + if !ok { + return nil, errors.New("missing KEEL_PRIVATE_KEY secret") + } + + privateKeyBlock, _ := pem.Decode([]byte(privateKeyPem)) + if privateKeyBlock == nil { + return nil, errors.New("error decoding private key PEM") + } + + privateKey, err := x509.ParsePKCS1PrivateKey(privateKeyBlock.Bytes) + if err != nil { + return nil, err + } + + return privateKey, nil +} + +func initLogger(level string) (*logrus.Logger, error) { + log := logrus.New() + log.SetFormatter(&logrus.JSONFormatter{}) + log.SetOutput(os.Stdout) + + l, err := logrus.ParseLevel(level) + if err != nil { + return nil, err + } + + log.SetLevel(l) + return log, nil +} + +func (h *Handler) buildContext(ctx context.Context) (context.Context, error) { + ctx = runtimectx.WithOAuthConfig(ctx, &h.config.Auth) + ctx = runtimectx.WithPrivateKey(ctx, h.privateKey) + ctx = runtimectx.WithSecrets(ctx, h.secrets) + ctx = runtimectx.WithStorage(ctx, h.filesStorage) + ctx = db.WithDatabase(ctx, h.db) + ctx = functions.WithFunctionsTransport(ctx, h.functionsTransport) + + ctx, err := events.WithEventHandler(ctx, h.sqsEventHandler) + if err != nil { + return nil, err + } + + return ctx, nil +} + +func SsmParameterName(projectName string, env string, paramName string) string { + // TODO: add /secret/ before param name + return fmt.Sprintf("/keel/%s/%s/%s", projectName, env, paramName) +} diff --git a/deploy/lambdas/runtime/jobs.go b/deploy/lambdas/runtime/jobs.go new file mode 100644 index 000000000..54e45eaec --- /dev/null +++ b/deploy/lambdas/runtime/jobs.go @@ -0,0 +1,159 @@ +package runtime + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/sirupsen/logrus" + "github.com/teamkeel/keel/functions" + "github.com/teamkeel/keel/runtime" + "github.com/teamkeel/keel/runtime/actions" + "github.com/teamkeel/keel/runtime/auth" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +type RunJobPayload struct { + ID string `json:"id"` + Name string `json:"name"` + Token string `json:"token"` +} + +type JobStatusWebhookPayload struct { + ID string `json:"id"` + Name string `json:"name"` + ProjectName string `json:"projectName"` + Env string `json:"env"` + Status string `json:"status"` + TraceID string `json:"traceId"` + Timestamp string `json:"timestamp"` +} + +const ( + JobStatusProcessing = "processing" + JobStatusSuccess = "success" + JobStatusFailed = "failed" +) + +func (h *Handler) JobHandler(ctx context.Context, event *RunJobPayload) error { + defer func() { + if h.tracerProvider != nil { + h.tracerProvider.ForceFlush(ctx) + } + }() + + ctx, span := h.tracer.Start(ctx, event.Name) + defer span.End() + + span.SetAttributes(attribute.String("job.name", event.Name)) + span.SetAttributes(attribute.String("job.id", event.ID)) + + log := h.log.WithFields(logrus.Fields{ + "jobName": event.Name, + }) + + job := h.schema.FindJob(event.Name) + if job == nil { + err := fmt.Errorf("no job found with name %s", event.Name) + return h.sendJobStatusWebhook(ctx, event, err, JobStatusFailed) + } + + log.Infof("Running job %s", job.Name) + + _ = h.sendJobStatusWebhook(ctx, event, nil, JobStatusProcessing) + + ctx, err := h.buildContext(ctx) + if err != nil { + return h.sendJobStatusWebhook(ctx, event, err, JobStatusFailed) + } + + if event.Token != "" { + identity, err := actions.HandleBearerToken(ctx, h.schema, event.Token) + if err != nil { + return h.sendJobStatusWebhook(ctx, event, err, JobStatusFailed) + } + + if identity != nil { + ctx = auth.WithIdentity(ctx, identity) + } + } + + inputs := map[string]any{} + if job.InputMessageName != "" { + if event.ID == "" { + err = fmt.Errorf("no ref provided but job requires inputs") + return h.sendJobStatusWebhook(ctx, event, err, JobStatusFailed) + } + + object, err := h.filesStorage.client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(h.filesStorage.bucketName), + Key: aws.String(fmt.Sprintf("jobs/%s", event.ID)), + }) + if err != nil { + return h.sendJobStatusWebhook(ctx, event, err, JobStatusFailed) + } + + b, err := io.ReadAll(object.Body) + if err != nil { + return h.sendJobStatusWebhook(ctx, event, err, JobStatusFailed) + } + + err = json.Unmarshal(b, &inputs) + if err != nil { + return h.sendJobStatusWebhook(ctx, event, err, JobStatusFailed) + } + } + + trigger := functions.ManualTrigger + if job.Schedule != nil { + trigger = functions.ScheduledTrigger + } + + err = runtime.NewJobHandler(h.schema).RunJob(ctx, job.Name, inputs, trigger) + if err != nil { + return h.sendJobStatusWebhook(ctx, event, err, JobStatusFailed) + } + + return h.sendJobStatusWebhook(ctx, event, err, JobStatusSuccess) +} + +func (h *Handler) sendJobStatusWebhook(ctx context.Context, event *RunJobPayload, err error, status string) error { + span := trace.SpanFromContext(ctx) + + if err != nil { + h.log.Errorf("job error: %s", err.Error()) + span.RecordError(err, trace.WithStackTrace(true)) + span.SetStatus(codes.Error, err.Error()) + } + + // If no webhook URL configured nothing to do + if h.args.JobsWebhookURL == "" { + return nil + } + + payload := &JobStatusWebhookPayload{ + ID: event.ID, + Name: event.Name, + ProjectName: h.args.ProjectName, + Env: h.args.Env, + Status: status, + TraceID: span.SpanContext().TraceID().String(), + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + + b, err := json.Marshal(payload) + if err != nil { + return err + } + + _, err = http.Post(h.args.JobsWebhookURL, "application/json", bytes.NewBuffer(b)) + return err +} diff --git a/deploy/lambdas/runtime/secrets.go b/deploy/lambdas/runtime/secrets.go new file mode 100644 index 000000000..bc2175033 --- /dev/null +++ b/deploy/lambdas/runtime/secrets.go @@ -0,0 +1,70 @@ +package runtime + +import ( + "context" + "os" + "strings" + "sync" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/aws/aws-sdk-go-v2/service/ssm/types" + "github.com/aws/aws-sdk-go/aws" + "github.com/samber/lo" + "golang.org/x/sync/errgroup" +) + +func initSecrets(ctx context.Context, names []string, awsEndpoint string) (map[string]string, error) { + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return nil, err + } + + opts := []func(*ssm.Options){} + if awsEndpoint != "" { + opts = append(opts, func(o *ssm.Options) { + o.BaseEndpoint = &awsEndpoint + }) + } + + secretNames := lo.Map(names, func(s string, _ int) string { + return SsmParameterName(os.Getenv("KEEL_PROJECT_NAME"), os.Getenv("KEEL_ENV"), s) + }) + + g, ctx := errgroup.WithContext(ctx) + params := []types.Parameter{} + var mutex sync.Mutex + + // Docs for GetParameters state "Maximum number of 10 items" + // https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_GetParameters.html#API_GetParameters_RequestSyntax + for _, chunk := range lo.Chunk(secretNames, 10) { + g.Go(func() error { + res, err := ssm.NewFromConfig(cfg, opts...).GetParameters(ctx, &ssm.GetParametersInput{ + Names: chunk, + WithDecryption: aws.Bool(true), + }) + if err != nil { + return err + } + + mutex.Lock() + defer mutex.Unlock() + params = append(params, res.Parameters...) + return nil + }) + } + + err = g.Wait() + if err != nil { + return nil, err + } + + secrets := map[string]string{} + for _, p := range params { + parts := strings.Split(*p.Name, "/") + name := parts[len(parts)-1] + secrets[name] = *p.Value + } + + return secrets, nil +} diff --git a/deploy/lambdas/runtime/tracing.go b/deploy/lambdas/runtime/tracing.go new file mode 100644 index 000000000..821c061f3 --- /dev/null +++ b/deploy/lambdas/runtime/tracing.go @@ -0,0 +1,32 @@ +package runtime + +import ( + "context" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" +) + +func initTracing() (trace.Tracer, *sdktrace.TracerProvider) { + tracerProvider := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(&NoopExporter{}), + ) + + otel.SetTracerProvider(tracerProvider) + otel.SetTextMapPropagator(propagation.TraceContext{}) + + return otel.Tracer("keel.xyz"), tracerProvider +} + +type NoopExporter struct { +} + +func (n *NoopExporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error { + return nil +} + +func (n *NoopExporter) Shutdown(ctx context.Context) error { + return nil +} diff --git a/deploy/logging.go b/deploy/logging.go new file mode 100644 index 000000000..513bf3a85 --- /dev/null +++ b/deploy/logging.go @@ -0,0 +1,67 @@ +package deploy + +import ( + "fmt" + "time" + + "github.com/charmbracelet/lipgloss" + "github.com/teamkeel/keel/colors" +) + +var ( + IconCross = "❌" + IconTick = colors.Green("✔").String() + IconPipe = colors.Yellow("|").String() + LogIndent = " " +) + +func log(format string, a ...any) { + fmt.Printf(LogIndent+format+"\n", a...) +} + +func heading(v string) { + style := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("15")). + Background(lipgloss.Color("#7D56F4")). + PaddingLeft(1). + PaddingRight(1) + + log("\n%s%s ", LogIndent, style.Render(v)) +} + +func orange(format string, a ...any) string { + v := fmt.Sprintf(format, a...) + return colors.Orange(v).String() +} + +func gray(format string, a ...any) string { + v := fmt.Sprintf(format, a...) + return colors.Gray(v).String() +} + +func green(format string, a ...any) string { + v := fmt.Sprintf(format, a...) + return colors.Green(v).String() +} + +func red(format string, a ...any) string { + v := fmt.Sprintf(format, a...) + return colors.Red(v).String() +} + +type Timing struct { + t time.Time +} + +func NewTiming() *Timing { + return &Timing{t: time.Now()} +} + +func (t *Timing) Since() string { + since := time.Since(t.t) + since = since - (since % time.Millisecond) + v := gray("(%s)", since.String()) + t.t = time.Now() + return v +} diff --git a/deploy/logs.go b/deploy/logs.go new file mode 100644 index 000000000..505288ae3 --- /dev/null +++ b/deploy/logs.go @@ -0,0 +1,229 @@ +package deploy + +import ( + "context" + "encoding/json" + "fmt" + "path/filepath" + "sort" + "strings" + "sync" + "time" + + "github.com/TylerBrock/colorjson" + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" + "github.com/teamkeel/keel/colors" + "github.com/teamkeel/keel/config" + "golang.org/x/sync/errgroup" +) + +type StreamLogsArgs struct { + ProjectRoot string + Env string + StartTime time.Time +} + +func StreamLogs(ctx context.Context, args *StreamLogsArgs) error { + configFiles, err := config.LoadAll(args.ProjectRoot) + if err != nil { + return err + } + + var projectConfig *config.ProjectConfig + for _, f := range configFiles { + if filepath.Base(f.Filename) == fmt.Sprintf("keelconfig.%s.yaml", args.Env) { + if f.Errors != nil { + return f.Errors + } + projectConfig = f.Config + } + } + if projectConfig == nil { + return fmt.Errorf("no keelconfig.%s.yaml file found", args.Env) + } + + cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(projectConfig.Deploy.Region)) + if err != nil { + return err + } + + pulumiConfig, err := setupPulumi(ctx, &SetupPulumiArgs{ + AwsConfig: cfg, + Config: projectConfig, + Env: args.Env, + }) + if err != nil { + return err + } + + outputs, err := getStackOutputs(ctx, pulumiConfig) + if err != nil { + return err + } + + logs := cloudwatchlogs.NewFromConfig(cfg) + + for err != nil { + g, ctx := errgroup.WithContext(ctx) + events := []types.FilteredLogEvent{} + var s sync.Mutex + lambdaNames := []string{ + outputs.ApiLambdaName, + outputs.SubscriberLambdaName, + outputs.JobsLambdaName, + outputs.FunctionsLambdaName, + } + + for _, name := range lambdaNames { + name := name + g.Go(func() error { + e, err := fetchLogs(ctx, logs, name, args) + if err != nil { + return err + } + s.Lock() + defer s.Unlock() + events = append(events, e...) + return nil + }) + } + + err = g.Wait() + if err != nil { + break + } + + if len(events) == 0 { + time.Sleep(time.Second * 5) + args.StartTime = args.StartTime.Add(time.Second) + continue + } + + sort.Slice(events, func(i, j int) bool { + return *events[i].Timestamp < *events[j].Timestamp + }) + + for _, e := range events { + log(*e.Message) + t := time.Unix(0, (*e.Timestamp * int64(time.Millisecond))) + t = t.Add(time.Second) + args.StartTime = t + } + } + + return err +} + +func fetchLogs(ctx context.Context, logs *cloudwatchlogs.Client, lambdaName string, args *StreamLogsArgs) ([]types.FilteredLogEvent, error) { + var nextToken *string + startTime := args.StartTime.UnixMilli() + events := []types.FilteredLogEvent{} + + f := colorjson.NewFormatter() + f.Indent = 2 + + for { + input := &cloudwatchlogs.FilterLogEventsInput{ + LogGroupName: aws.String(fmt.Sprintf("/aws/lambda/%s", lambdaName)), + StartTime: &startTime, + NextToken: nextToken, + } + + out, err := logs.FilterLogEvents(ctx, input) + if err != nil { + return nil, err + } + + for _, e := range out.Events { + m := formatLog(lambdaName, *e.Message) + if m == "" { + continue + } + + e.Message = aws.String(formatLog(lambdaName, *e.Message)) + events = append(events, e) + } + + nextToken = out.NextToken + if nextToken == nil { + return events, nil + } + } +} + +func formatLog(lambdaName string, rawMessage string) string { + log := map[string]any{} + err := json.Unmarshal([]byte(rawMessage), &log) + if err != nil { + return rawMessage + } + + // Drop the Lambda platform.* logs + // TODO: maybe add a CLI flag for including these? + t, ok := log["type"].(string) + if ok && strings.HasPrefix(t, "platform.") { + return "" + } + + f := colorjson.NewFormatter() + f.Indent = 2 + + result := "" + + nameParts := strings.Split(lambdaName, "-") + name := strings.Join(nameParts[0:len(nameParts)-1], "-") + result += colors.Magenta(fmt.Sprintf("[%s]", name)).String() + + ts, ok := log["time"].(string) + if !ok { + ts, ok = log["timestamp"].(string) + } + if ok { + result += fmt.Sprintf(" %s", colors.Gray(fmt.Sprintf("[%s]", ts)).String()) + } + + level, ok := log["level"].(string) + if ok { + result += fmt.Sprintf(" %s", colors.Yellow(strings.ToUpper(level)).String()) + } + + message, ok := log["message"] + if !ok { + message, ok = log["msg"] + } + if ok { + m := map[string]any{} + switch v := message.(type) { + case string: + _ = json.Unmarshal([]byte(v), &m) + case map[string]any: + m = v + } + if len(m) > 0 { + b, err := f.Marshal(m) + if err == nil { + message = string(b) + } + } + result += fmt.Sprintf(" %s", message) + } + + rest := map[string]any{} + for k, v := range log { + switch k { + case "msg", "message", "requestId", "time", "timestamp", "level": + // nothing + default: + rest[k] = v + } + } + if len(rest) > 0 { + b, _ := f.Marshal(rest) + result += fmt.Sprintf(" %s", string(b)) + } + + return result +} diff --git a/deploy/migrations.go b/deploy/migrations.go new file mode 100644 index 000000000..85488cc49 --- /dev/null +++ b/deploy/migrations.go @@ -0,0 +1,114 @@ +package deploy + +import ( + "context" + "fmt" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/pulumi/pulumi/sdk/v3/go/auto" + "github.com/teamkeel/keel/db" + "github.com/teamkeel/keel/migrations" + "github.com/teamkeel/keel/proto" +) + +type RunMigrationsArgs struct { + AwsConfig aws.Config + Stack auto.Stack + Schema *proto.Schema + DryRun bool +} + +func runMigrations(ctx context.Context, args *RunMigrationsArgs) error { + t := NewTiming() + + // TODO: support external database by fetching DATABASE_URL secret from SSM + stackOutputs, err := args.Stack.Outputs(ctx) + if err != nil { + log("%s error getting stack outputs: %s", IconCross, err.Error()) + return err + } + + outputs := parseStackOutputs(stackOutputs) + + if outputs.DatabaseSecretArn == "" { + log("%s no database secret ARN in stack outputs - skipping migrations check", IconPipe) + return nil + } + + databaseURL, err := GetRDSConnection(ctx, &GetRDSConnectionArgs{ + Cfg: args.AwsConfig, + Endpoint: outputs.DatabaseEndpoint, + DbName: outputs.DatabaseDbName, + SecretArn: outputs.DatabaseSecretArn, + }) + if err != nil { + log("%s error getting RDS connection string: %s", IconCross, err.Error()) + return err + } + + dbConn, err := db.New(ctx, databaseURL) + if err != nil { + log("%s error connecting to the database: %s", IconCross, err.Error()) + return err + } + + m, err := migrations.New(ctx, args.Schema, dbConn) + if err != nil { + log("%s error generating database migrations: %s", IconCross, err.Error()) + return err + } + + err = m.Apply(ctx, args.DryRun) + if err != nil { + message := "Error applying database migrations" + if args.DryRun { + message = "Database migrations can not be applied" + } + if strings.Contains(err.Error(), "contains null values") { + message = fmt.Sprintf(`%s. + + The most likely cause of this is that you have added a non-null field to a model for which there are already rows in the database. + To fix this either make the new field optional (by using '?') or provide a default value using @default.`, message) + } + log("%s %s: %s", IconCross, message, err.Error()) + return err + } + + if args.DryRun { + switch { + case len(m.Changes) == 0: + log("%s No database schema changes required %s", IconTick, t.Since()) + default: + log("%s The following database schema changes will be applied %s", IconTick, t.Since()) + for _, ch := range m.Changes { + message := ch.Model + if ch.Field != "" { + message = fmt.Sprintf("%s.%s", message, ch.Field) + } + + action := "" + switch ch.Type { + case migrations.ChangeTypeAdded: + action = green("(added)") + case migrations.ChangeTypeRemoved: + action = red("(removed)") + case migrations.ChangeTypeModified: + action = gray("(modified)") + } + + log(" - %s %s", orange(message), action) + } + } + return nil + } + + switch { + case len(m.Changes) == 0: + log("%s Databaase schema is up-to-date %s", IconTick, t.Since()) + default: + log("%s %s database schema changes applied %s", IconTick, orange("%d", len(m.Changes)), t.Since()) + } + + return nil +} diff --git a/deploy/program.go b/deploy/program.go new file mode 100644 index 000000000..80657cdcc --- /dev/null +++ b/deploy/program.go @@ -0,0 +1,581 @@ +package deploy + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + pulumiaws "github.com/pulumi/pulumi-aws/sdk/v6/go/aws" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/ec2" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/iam" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/lambda" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/rds" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/s3" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/scheduler" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/sqs" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/samber/lo" + "github.com/teamkeel/keel/config" + "github.com/teamkeel/keel/deploy/lambdas/runtime" + "github.com/teamkeel/keel/proto" +) + +// https://github.com/open-telemetry/opentelemetry-lambda/releases/tag/layer-collector%2F0.12.0 +const otelCollectorLayer = "arn:aws:lambda:%s:184161586896:layer:opentelemetry-collector-amd64-0_12_0:1" + +type NewProgramArgs struct { + AwsConfig aws.Config + AwsAccountID string + RuntimeLambdaPath string + FunctionsLambdaPath string + Env string + Config *config.ProjectConfig + Schema *proto.Schema +} + +func createProgram(args *NewProgramArgs) pulumi.RunFunc { + return func(ctx *pulumi.Context) error { + deployCfg := args.Config.Deploy + + projectName := deployCfg.ProjectName + region := deployCfg.Region + + baseTags := pulumi.StringMap{ + "Project": pulumi.String(projectName), + "Env": pulumi.String(args.Env), + } + + vpc, err := ec2.NewVpc(ctx, "vpc", &ec2.VpcArgs{ + CidrBlock: pulumi.String("10.0.0.0/16"), + EnableDnsHostnames: pulumi.BoolPtr(true), + Tags: extendStringMap(baseTags, pulumi.StringMap{ + "Name": pulumi.String(fmt.Sprintf("keel-%s-%s-vpc", projectName, args.Env)), + }), + }) + if err != nil { + return err + } + + igw, err := ec2.NewInternetGateway(ctx, "internet-gateway", &ec2.InternetGatewayArgs{ + VpcId: vpc.ID(), + Tags: extendStringMap(baseTags, pulumi.StringMap{ + "Name": pulumi.String(fmt.Sprintf("keel-%s-%s-internet-gateway", projectName, args.Env)), + }), + }) + if err != nil { + return err + } + + azs := pulumiaws.GetAvailabilityZonesOutput(ctx, pulumiaws.GetAvailabilityZonesOutputArgs{ + State: pulumi.String("available"), + }) + subnetIDs := azs.Names().ApplyT(func(names []string) (pulumi.StringArray, error) { + result := pulumi.StringArray{} + + for i, zone := range names[:2] { + subnet, err := ec2.NewSubnet(ctx, fmt.Sprintf("subnet-%d", i+1), &ec2.SubnetArgs{ + VpcId: vpc.ID(), + CidrBlock: pulumi.String(fmt.Sprintf("10.0.%d.0/22", 8*i)), + MapPublicIpOnLaunch: pulumi.Bool(true), + AvailabilityZone: pulumi.String(zone), + Tags: extendStringMap(baseTags, pulumi.StringMap{ + "Name": pulumi.String(fmt.Sprintf("keel-%s-%s-subnet-%d", projectName, args.Env, i+1)), + }), + }) + if err != nil { + return nil, err + } + + routeTable, err := ec2.NewRouteTable(ctx, fmt.Sprintf("route-table-%d", i+1), &ec2.RouteTableArgs{ + VpcId: vpc.ID(), + Routes: ec2.RouteTableRouteArray{ + &ec2.RouteTableRouteArgs{ + CidrBlock: pulumi.String("0.0.0.0/0"), // Route all traffic + GatewayId: igw.ID(), // To the Internet Gateway + }, + }, + Tags: extendStringMap(baseTags, pulumi.StringMap{ + "Name": pulumi.String(fmt.Sprintf("keel-%s-%s-route-table-%d", projectName, args.Env, i+1)), + }), + }) + if err != nil { + return nil, err + } + + _, err = ec2.NewRouteTableAssociation(ctx, fmt.Sprintf("route-table-association-%d", i+1), &ec2.RouteTableAssociationArgs{ + SubnetId: subnet.ID(), + RouteTableId: routeTable.ID(), + }) + if err != nil { + return nil, err + } + + result = append(result, subnet.ID().ToStringOutput()) + } + + return result, nil + }).(pulumi.StringArrayOutput) + + dbSubnetGroup, err := rds.NewSubnetGroup(ctx, "subnet-group", &rds.SubnetGroupArgs{ + SubnetIds: subnetIDs, + Tags: baseTags, + }) + if err != nil { + return err + } + + securityGroup, err := ec2.NewSecurityGroup(ctx, "db-security-group", &ec2.SecurityGroupArgs{ + VpcId: vpc.ID(), + Ingress: ec2.SecurityGroupIngressArray{ + &ec2.SecurityGroupIngressArgs{ + Protocol: pulumi.String("tcp"), + FromPort: pulumi.Int(5432), + ToPort: pulumi.Int(5432), + CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")}, + }, + }, + Egress: ec2.SecurityGroupEgressArray{ + &ec2.SecurityGroupEgressArgs{ + Protocol: pulumi.String("-1"), + FromPort: pulumi.Int(0), + ToPort: pulumi.Int(0), + CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")}, + }, + }, + Tags: baseTags, + }) + if err != nil { + return err + } + + // default to the cheapest instance available + rdsInstanceType := "db.t4g.micro" + rdsStorage := 20 + rdsMultiAz := false + + // update from config + if deployCfg.Database != nil { + db := deployCfg.Database + if db.RDS != nil && db.RDS.Instance != nil { + rdsInstanceType = *db.RDS.Instance + } + if db.RDS != nil && db.RDS.Storage != nil { + rdsStorage = *db.RDS.Storage + } + if db.RDS != nil && db.RDS.MultiAZ != nil { + rdsMultiAz = *db.RDS.MultiAZ + } + } + + // Create an RDS PostgreSQL instance + dbInstance, err := rds.NewInstance(ctx, "rds", &rds.InstanceArgs{ + Engine: pulumi.String("postgres"), + InstanceClass: pulumi.String(rdsInstanceType), + AllocatedStorage: pulumi.Int(rdsStorage), + MultiAz: pulumi.BoolPtr(rdsMultiAz), + DbSubnetGroupName: dbSubnetGroup.Name, + VpcSecurityGroupIds: pulumi.StringArray{ + securityGroup.ID(), + }, + DbName: pulumi.String("keel"), + Username: pulumi.String("keel"), + ManageMasterUserPassword: pulumi.BoolPtr(true), + SkipFinalSnapshot: pulumi.Bool(true), + PubliclyAccessible: pulumi.Bool(true), + Tags: baseTags, + }) + if err != nil { + return err + } + + dbSecretArn := dbInstance.MasterUserSecrets.Index(pulumi.Int(0)).SecretArn() + + ctx.Export(StackOutputDatabaseEndpoint, dbInstance.Endpoint) + ctx.Export(StackOutputDatabaseDbName, dbInstance.DbName) + ctx.Export(StackOutputDatabaseSecretArn, dbSecretArn) + + filesBucket, err := s3.NewBucket(ctx, "file-storage", &s3.BucketArgs{ + BucketPrefix: pulumi.StringPtr(fmt.Sprintf("%s--%s-", projectName, args.Env)), + Tags: baseTags, + }, pulumi.RetainOnDelete(true)) + if err != nil { + return err + } + + queue, err := sqs.NewQueue(ctx, "events", &sqs.QueueArgs{ + Tags: baseTags, + }) + if err != nil { + return err + } + + functionsRole, err := createLambdaRole(ctx, "functions", iam.GetPolicyDocumentStatementArray{ + iam.GetPolicyDocumentStatementInput(iam.GetPolicyDocumentStatementArgs{ + Actions: pulumi.ToStringArray([]string{ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject", + }), + Resources: pulumi.ToStringArrayOutput( + []pulumi.StringOutput{ + filesBucket.Arn.ApplyT(func(v string) string { + return v + "/*" + }).(pulumi.StringOutput), + }, + ), + }), + }, baseTags) + if err != nil { + return err + } + + // OTEL config + var otelLayer pulumi.StringArray + if deployCfg.Telemetry != nil && deployCfg.Telemetry.Collector != "" { + arn := fmt.Sprintf(otelCollectorLayer, region) + otelLayer = pulumi.ToStringArray([]string{arn}) + } + + functionEnvVars := pulumi.StringMap{ + "KEEL_PROJECT_NAME": pulumi.String(projectName), + "KEEL_ENV": pulumi.String(args.Env), + "KEEL_FILES_BUCKET_NAME": filesBucket.Bucket, + // The actual connection string is passed from the runtime to functions + // via a secret + "KEEL_DB_CONN_TYPE": pulumi.String("pg"), + "KEEL_DB_CERT": pulumi.String("/var/task/rds.pem"), + "NODE_OPTIONS": pulumi.String("--enable-source-maps"), + } + + // OTEL config + if deployCfg.Telemetry != nil && deployCfg.Telemetry.Collector != "" { + functionEnvVars["OPENTELEMETRY_COLLECTOR_CONFIG_FILE"] = pulumi.String("/var/task/collector.yaml") + } + + // Add env vars from config + for _, env := range args.Config.Environment { + functionEnvVars[env.Name] = pulumi.String(env.Value) + } + + functions, err := lambda.NewFunction(ctx, "functions", &lambda.FunctionArgs{ + Code: pulumi.NewFileArchive(args.FunctionsLambdaPath), + Runtime: lambda.RuntimeNodeJS20dX, + MemorySize: pulumi.IntPtr(2048), + Role: functionsRole.Arn, + Handler: pulumi.String("main.handler"), + Environment: lambda.FunctionEnvironmentArgs{ + Variables: functionEnvVars, + }, + Layers: otelLayer, + LoggingConfig: lambda.FunctionLoggingConfigArgs{ + LogFormat: pulumi.String("JSON"), + }, + Tags: baseTags, + }) + if err != nil { + return fmt.Errorf("error creating runtime lambda: %v", err) + } + + runtimeRole, err := createLambdaRole(ctx, "runtime", iam.GetPolicyDocumentStatementArray{ + iam.GetPolicyDocumentStatementInput(iam.GetPolicyDocumentStatementArgs{ + Actions: pulumi.ToStringArray([]string{ + "secretsmanager:GetSecretValue", + }), + Resources: pulumi.ToStringArrayOutput([]pulumi.StringOutput{ + dbSecretArn.Elem(), + }), + }), + iam.GetPolicyDocumentStatementInput(iam.GetPolicyDocumentStatementArgs{ + Actions: pulumi.ToStringArray([]string{ + "ssm:GetParameter", + "ssm:GetParameters", + }), + Resources: pulumi.ToStringArray([]string{ + fmt.Sprintf("arn:aws:ssm:%s:%s:parameter%s", + region, + args.AwsAccountID, + runtime.SsmParameterName(projectName, args.Env, "*")), + }), + }), + iam.GetPolicyDocumentStatementInput(iam.GetPolicyDocumentStatementArgs{ + Actions: pulumi.ToStringArray([]string{ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject", + }), + Resources: pulumi.ToStringArrayOutput( + []pulumi.StringOutput{ + filesBucket.Arn.ApplyT(func(v string) string { + return v + "/*" + }).(pulumi.StringOutput), + }, + ), + }), + iam.GetPolicyDocumentStatementInput(iam.GetPolicyDocumentStatementArgs{ + Actions: pulumi.ToStringArray([]string{ + "lambda:InvokeFunction", + }), + Resources: pulumi.ToStringArrayOutput( + []pulumi.StringOutput{ + functions.Arn, + }, + ), + }), + iam.GetPolicyDocumentStatementInput(iam.GetPolicyDocumentStatementArgs{ + Actions: pulumi.ToStringArray([]string{ + "sqs:SendMessage", + "sqs:GetQueueUrl", + "sqs:DeleteMessage", + "sqs:ReceiveMessage", + "sqs:GetQueueAttributes", + }), + Resources: pulumi.ToStringArrayOutput( + []pulumi.StringOutput{ + queue.Arn, + }, + ), + }), + }, baseTags) + if err != nil { + return err + } + + jobsWebhookURL := "" + if deployCfg.Jobs != nil { + jobsWebhookURL = deployCfg.Jobs.WebhookURL + } + + secretNames := lo.Map(args.Config.Secrets, func(s config.Secret, _ int) string { + return s.Name + }) + secretNames = append(secretNames, "KEEL_PRIVATE_KEY") + + baseRuntimeEnvVars := pulumi.StringMap{ + "KEEL_PROJECT_NAME": pulumi.String(projectName), + "KEEL_ENV": pulumi.String(args.Env), + "KEEL_DATABASE_ENDPOINT": dbInstance.Endpoint, + "KEEL_DATABASE_DB_NAME": dbInstance.DbName, + "KEEL_DATABASE_SECRET_ARN": dbSecretArn.Elem(), + "KEEL_SECRETS": pulumi.String(strings.Join(secretNames, ":")), + "KEEL_FILES_BUCKET_NAME": filesBucket.Bucket, + "KEEL_FUNCTIONS_ARN": functions.Arn, + "KEEL_QUEUE_URL": queue.Url, + "KEEL_JOBS_WEBHOOK_URL": pulumi.String(jobsWebhookURL), + } + + // OTEL config + if deployCfg.Telemetry != nil && deployCfg.Telemetry.Collector != "" { + baseRuntimeEnvVars["OPENTELEMETRY_COLLECTOR_CONFIG_FILE"] = pulumi.String("/var/task/collector.yaml") + } + + // Add env vars from config + for _, env := range args.Config.Environment { + baseRuntimeEnvVars[env.Name] = pulumi.String(env.Value) + } + + api, err := lambda.NewFunction(ctx, "api", &lambda.FunctionArgs{ + Code: pulumi.NewFileArchive(args.RuntimeLambdaPath), + Runtime: lambda.RuntimeCustomAL2023, + MemorySize: pulumi.IntPtr(2048), + Role: runtimeRole.Arn, + Handler: pulumi.String("main"), + Environment: lambda.FunctionEnvironmentArgs{ + Variables: extendStringMap(baseRuntimeEnvVars, pulumi.StringMap{ + "KEEL_RUNTIME_MODE": pulumi.String(runtime.RuntimeModeApi), + }), + }, + Layers: otelLayer, + LoggingConfig: lambda.FunctionLoggingConfigArgs{ + LogFormat: pulumi.String("JSON"), + }, + Tags: baseTags, + }) + if err != nil { + return fmt.Errorf("error creating api lambda: %v", err) + } + + subscriber, err := lambda.NewFunction(ctx, "subscriber", &lambda.FunctionArgs{ + Code: pulumi.NewFileArchive(args.RuntimeLambdaPath), + Runtime: lambda.RuntimeCustomAL2023, + MemorySize: pulumi.IntPtr(2048), + Role: runtimeRole.Arn, + Handler: pulumi.String("main"), + Environment: lambda.FunctionEnvironmentArgs{ + Variables: extendStringMap(baseRuntimeEnvVars, pulumi.StringMap{ + "KEEL_RUNTIME_MODE": pulumi.String(runtime.RuntimeModeSubscriber), + }), + }, + Layers: otelLayer, + LoggingConfig: lambda.FunctionLoggingConfigArgs{ + LogFormat: pulumi.String("JSON"), + }, + Tags: baseTags, + }) + if err != nil { + return fmt.Errorf("error creating subscriber lambda: %v", err) + } + + _, err = lambda.NewEventSourceMapping(ctx, "subscriber-event-source-mapping", &lambda.EventSourceMappingArgs{ + EventSourceArn: queue.Arn, + FunctionName: subscriber.Arn, + Tags: baseTags, + }) + if err != nil { + return err + } + + jobs, err := lambda.NewFunction(ctx, "jobs", &lambda.FunctionArgs{ + Code: pulumi.NewFileArchive(args.RuntimeLambdaPath), + Runtime: lambda.RuntimeCustomAL2023, + MemorySize: pulumi.IntPtr(2048), + Role: runtimeRole.Arn, + Handler: pulumi.String("main"), + Layers: otelLayer, + Environment: lambda.FunctionEnvironmentArgs{ + Variables: extendStringMap(baseRuntimeEnvVars, pulumi.StringMap{ + "KEEL_RUNTIME_MODE": pulumi.String(runtime.RuntimeModeJob), + }), + }, + LoggingConfig: lambda.FunctionLoggingConfigArgs{ + LogFormat: pulumi.String("JSON"), + }, + Tags: baseTags, + }) + if err != nil { + return fmt.Errorf("error creating jobs lambda: %v", err) + } + + for _, job := range args.Schema.Jobs { + if job.Schedule == nil { + continue + } + + expression := fmt.Sprintf("cron(%s)", strings.ReplaceAll(job.Schedule.Expression, "\"", "")) + + jobJson, err := json.Marshal(map[string]any{ + "name": job.Name, + }) + if err != nil { + return err + } + + schedule, err := scheduler.NewSchedule(ctx, fmt.Sprintf("scheduled-job-%s", job.Name), &scheduler.ScheduleArgs{ + ScheduleExpression: pulumi.String(expression), + FlexibleTimeWindow: scheduler.ScheduleFlexibleTimeWindowArgs{ + Mode: pulumi.String("OFF"), + }, + Target: &scheduler.ScheduleTargetArgs{ + Arn: jobs.Arn, + RoleArn: runtimeRole.Arn, + Input: pulumi.StringPtr(string(jobJson)), + }, + StartDate: pulumi.StringPtr(time.Now().UTC().Format(time.RFC3339)), + }) + if err != nil { + return err + } + + _, err = lambda.NewPermission(ctx, fmt.Sprintf("scheduled-job-%s-permission", job.Name), &lambda.PermissionArgs{ + Action: pulumi.String("lambda:InvokeFunction"), + Function: jobs.Name, + Principal: pulumi.String("scheduler.amazonaws.com"), + SourceArn: schedule.Arn, + }) + if err != nil { + return err + } + } + + apiURL, err := lambda.NewFunctionUrl(ctx, "api-url", &lambda.FunctionUrlArgs{ + AuthorizationType: pulumi.String("NONE"), + Cors: &lambda.FunctionUrlCorsArgs{ + AllowCredentials: pulumi.BoolPtr(true), + AllowHeaders: pulumi.ToStringArray([]string{"*"}), + AllowMethods: pulumi.ToStringArray([]string{"*"}), + AllowOrigins: pulumi.ToStringArray([]string{"*"}), + ExposeHeaders: pulumi.ToStringArray([]string{"*"}), + }, + FunctionName: api.Name, + }) + if err != nil { + return fmt.Errorf("error creating runtime lambda URL: %v", err) + } + + ctx.Export(StackOutputApiURL, apiURL.FunctionUrl) + ctx.Export(StackOutputApiLambdaName, api.Name) + ctx.Export(StackOutputSubscriberLambdaName, subscriber.Name) + ctx.Export(StackOutputFunctionsLambdaName, functions.Name) + return nil + } +} + +func createLambdaRole(ctx *pulumi.Context, prefix string, statements iam.GetPolicyDocumentStatementArray, tags pulumi.StringMap) (*iam.Role, error) { + role, err := iam.NewRole(ctx, fmt.Sprintf("%s-role", prefix), &iam.RoleArgs{ + AssumeRolePolicy: pulumi.String(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Principal": { + "Service": "lambda.amazonaws.com" + }, + "Effect": "Allow", + "Sid": "" + }, + { + "Action": "sts:AssumeRole", + "Principal": { + "Service": "scheduler.amazonaws.com" + }, + "Effect": "Allow", + "Sid": "" + } + ] + }`), + Tags: tags, + }) + if err != nil { + return nil, fmt.Errorf("error creating IAM role: %v", err) + } + + lambdaPolicy, err := iam.NewPolicy(ctx, fmt.Sprintf("%s-policy", prefix), &iam.PolicyArgs{ + Policy: iam.GetPolicyDocumentOutput(ctx, iam.GetPolicyDocumentOutputArgs{ + Statements: statements, + }).Json(), + Tags: tags, + }) + if err != nil { + return nil, fmt.Errorf("error creating role policy: %v", err) + } + + _, err = iam.NewRolePolicyAttachment(ctx, fmt.Sprintf("%s-managed-policy", prefix), &iam.RolePolicyAttachmentArgs{ + Role: role.Name, + PolicyArn: pulumi.String("arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"), + }) + if err != nil { + return nil, fmt.Errorf("error attaching managed policy to role: %v", err) + } + + _, err = iam.NewRolePolicyAttachment(ctx, fmt.Sprintf("%s-policy-attachement", prefix), &iam.RolePolicyAttachmentArgs{ + Role: role.Name, + PolicyArn: lambdaPolicy.Arn, + }) + if err != nil { + return nil, fmt.Errorf("error attaching custom policy to role: %v", err) + } + + return role, nil +} + +func extendStringMap(a, b pulumi.StringMap) pulumi.StringMap { + r := pulumi.StringMap{} + for k, v := range a { + r[k] = v + } + for k, v := range b { + r[k] = v + } + return r +} diff --git a/deploy/run.go b/deploy/run.go new file mode 100644 index 000000000..6621e1a72 --- /dev/null +++ b/deploy/run.go @@ -0,0 +1,196 @@ +package deploy + +import ( + "context" + "errors" + "fmt" + "strings" + + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/smithy-go" + "github.com/pulumi/pulumi/sdk/v3/go/auto/events" + "github.com/pulumi/pulumi/sdk/v3/go/auto/optdestroy" + "github.com/pulumi/pulumi/sdk/v3/go/auto/optup" + "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" +) + +const ( + UpAction = "up" + RemoveAction = "remove" +) + +type RunArgs struct { + Action string + ProjectRoot string + Env string + RuntimeBinary string +} + +func Run(ctx context.Context, args *RunArgs) error { + buildLambdasResult, err := Build(ctx, &BuildArgs{ + ProjectRoot: args.ProjectRoot, + Env: args.Env, + RuntimeBinary: args.RuntimeBinary, + }) + if err != nil { + return err + } + + protoSchema := buildLambdasResult.Schema + projectConfig := buildLambdasResult.Config + + deploy := projectConfig.Deploy + if deploy == nil { + err = fmt.Errorf("%s missing 'deploy' section in Keel config file", IconCross) + log("%s %s", IconCross, err.Error()) + return err + } + + if args.Action == UpAction { + heading("Deploy") + } else { + heading("Remove") + } + + cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(projectConfig.Deploy.Region)) + if err != nil { + log("%s error loading AWS config", IconCross, err.Error()) + return err + } + + awsIdentity, err := getAwsIdentity(ctx, cfg) + if err != nil { + return err + } + + log("%s AWS account %s", IconTick, orange(awsIdentity.AccountID)) + + if args.Action == UpAction { + err = createPrivateKeySecret(ctx, &CreatePrivateKeySecretArgs{ + AwsConfig: cfg, + ProjectName: projectConfig.Deploy.ProjectName, + Env: args.Env, + }) + if err != nil { + return err + } + } + + pulumiCfg, err := setupPulumi(ctx, &SetupPulumiArgs{ + AwsConfig: cfg, + Config: projectConfig, + Env: args.Env, + }) + if err != nil { + return err + } + + stack, err := selectStack(ctx, &SelectStackArgs{ + Env: args.Env, + Schema: protoSchema, + Config: projectConfig, + AwsConfig: cfg, + AccountID: awsIdentity.AccountID, + RuntimeLambdaPath: buildLambdasResult.RuntimePath, + FunctionsLambdaPath: buildLambdasResult.FunctionsPath, + PulumiConfig: pulumiCfg, + }) + if err != nil { + return err + } + + if args.Action == UpAction { + err = runMigrations(ctx, &RunMigrationsArgs{ + AwsConfig: cfg, + Stack: stack, + Schema: protoSchema, + DryRun: true, + }) + if err != nil { + return err + } + } + + eventsChannel := make(chan events.EngineEvent) + go func() { + pending := map[string]*Timing{} + for event := range eventsChannel { + switch { + case event.ResourcePreEvent != nil: + urn := event.ResourcePreEvent.Metadata.URN + if event.ResourcePreEvent.Metadata.Op != apitype.OpSame { + pending[urn] = NewTiming() + urn := cleanURN(urn) + log("%s %s - %s", IconPipe, gray(urn), orange("%s", event.ResourcePreEvent.Metadata.Op)) + } + case event.ResOutputsEvent != nil: + urn := event.ResOutputsEvent.Metadata.URN + if t, ok := pending[urn]; ok { + urn := cleanURN(urn) + log("%s %s - %s %s", IconPipe, gray(urn), green("done"), t.Since()) + } + default: + // for debugging + // b, _ := json.Marshal(event) + // log("Pulumi event: %s", string(b)) + } + } + }() + + t := NewTiming() + + if args.Action == RemoveAction { + log("%s Removing resources...", IconPipe) + _, err := stack.Destroy(ctx, optdestroy.EventStreams(eventsChannel)) + if err != nil { + log("%s Error removing stack: %s", IconCross, err.Error()) + return err + } + + log("%s Stack removed %s", IconTick, t.Since()) + return nil + } + + log("%s Updating resources...", IconPipe) + res, err := stack.Up(ctx, optup.EventStreams(eventsChannel)) + if err != nil { + log("%s error deploying stack: %s", IconCross, err.Error()) + return err + } + + log("%s App successfully deployed %s", IconTick, t.Since()) + + err = runMigrations(ctx, &RunMigrationsArgs{ + AwsConfig: cfg, + Stack: stack, + Schema: protoSchema, + DryRun: false, + }) + if err != nil { + return err + } + + // get the URL from the stack outputs + url, ok := res.Outputs["apiUrl"].Value.(string) + if !ok { + err := fmt.Errorf("error getting API url from stack outputs") + log("%s %s", IconCross, err.Error()) + return err + } + + log("%s API URL: %s", IconTick, orange(url)) + return nil +} + +func cleanURN(s string) string { + parts := strings.Split(s, "::") + if len(parts) < 3 { + return s + } + return strings.Join(parts[2:], "::") +} + +func isSmithyAPIError(err error, code string) bool { + var ae smithy.APIError + return !errors.As(err, &ae) && ae.ErrorCode() == code +} diff --git a/deploy/secrets.go b/deploy/secrets.go new file mode 100644 index 000000000..99ecf669e --- /dev/null +++ b/deploy/secrets.go @@ -0,0 +1,176 @@ +package deploy + +import ( + "context" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/aws/aws-sdk-go-v2/service/ssm/types" + "github.com/teamkeel/keel/deploy/lambdas/runtime" +) + +type SetSecretArgs struct { + ProjectRoot string + Env string + Key string + Value string +} + +func SetSecret(ctx context.Context, args *SetSecretArgs) error { + client, name, err := getSsmClient(ctx, &getSssmClient{ + projectRoot: args.ProjectRoot, + env: args.Env, + key: args.Key, + }) + if err != nil { + return err + } + + _, err = client.PutParameter(ctx, &ssm.PutParameterInput{ + Name: aws.String(name), + Value: aws.String(args.Value), + Overwrite: aws.Bool(true), + Type: types.ParameterTypeSecureString, + }) + if err != nil { + log("%s errors setting secret in AWS: %s", IconCross, err.Error()) + return err + } + + return nil +} + +type GetSecretArgs struct { + ProjectRoot string + Env string + Key string +} + +func GetSecret(ctx context.Context, args *GetSecretArgs) (*types.Parameter, error) { + client, name, err := getSsmClient(ctx, &getSssmClient{ + projectRoot: args.ProjectRoot, + env: args.Env, + key: args.Key, + }) + if err != nil { + return nil, err + } + + p, err := client.GetParameter(ctx, &ssm.GetParameterInput{ + Name: aws.String(name), + WithDecryption: aws.Bool(true), + }) + if err != nil { + if !isSmithyAPIError(err, "ParameterNotFound") { + log("%s error fetching secret from SSM: %s", IconCross, err.Error()) + return nil, err + } + log("%s secret '%s' not set", IconCross, args.Key) + return nil, err + } + + log(*p.Parameter.Value) + return p.Parameter, nil +} + +type ListSecretsArgs struct { + ProjectRoot string + Env string +} + +func ListSecrets(ctx context.Context, args *ListSecretsArgs) ([]types.Parameter, error) { + client, name, err := getSsmClient(ctx, &getSssmClient{ + projectRoot: args.ProjectRoot, + env: args.Env, + key: "", + }) + if err != nil { + return nil, err + } + + // don't need final slash + path := strings.TrimSuffix(name, "/") + + var token *string + params := []types.Parameter{} + for { + p, err := client.GetParametersByPath(ctx, &ssm.GetParametersByPathInput{ + Path: aws.String(path), + WithDecryption: aws.Bool(true), + NextToken: token, + }) + if err != nil { + log("%s error listing secrets from AWS: %s", IconCross, err.Error()) + return nil, err + } + + params = append(params, p.Parameters...) + if p.NextToken != nil { + token = p.NextToken + } + if token == nil { + break + } + } + + return params, nil +} + +type DeleteSecretArgs struct { + ProjectRoot string + Env string + Key string +} + +func DeleteSecret(ctx context.Context, args *DeleteSecretArgs) error { + client, name, err := getSsmClient(ctx, &getSssmClient{ + projectRoot: args.ProjectRoot, + env: args.Env, + key: args.Key, + }) + if err != nil { + return err + } + + _, err = client.DeleteParameter(ctx, &ssm.DeleteParameterInput{ + Name: aws.String(name), + }) + if err != nil { + if !isSmithyAPIError(err, "ParameterNotFound") { + log("%s error deleting secret from SSM: %s", IconCross, err.Error()) + return err + } + log("%s secret '%s' not set", IconPipe, args.Key) + return nil + } + + return nil +} + +type getSssmClient struct { + projectRoot string + env string + key string +} + +func getSsmClient(ctx context.Context, args *getSssmClient) (client *ssm.Client, name string, err error) { + projectConfig, err := loadKeelConfig(&LoadKeelConfigArgs{ + ProjectRoot: args.projectRoot, + Env: args.env, + }) + if err != nil { + return nil, "", err + } + + cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(projectConfig.Deploy.Region)) + if err != nil { + log("%s error loading AWS config: %s", IconCross, err.Error()) + return nil, "", err + } + + paramName := runtime.SsmParameterName(projectConfig.Deploy.ProjectName, args.env, args.key) + + return ssm.NewFromConfig(cfg), paramName, nil +} diff --git a/deploy/setup.go b/deploy/setup.go new file mode 100644 index 000000000..a38957d8d --- /dev/null +++ b/deploy/setup.go @@ -0,0 +1,305 @@ +package deploy + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/aws/aws-sdk-go-v2/service/ssm" + ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types" + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/aws/smithy-go" + "github.com/pulumi/pulumi/sdk/v3/go/auto" + "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" + "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" + "github.com/teamkeel/keel/config" + "github.com/teamkeel/keel/deploy/lambdas/runtime" +) + +type Output struct { + Heading string + Icon string + Message string + Error error +} + +const ( + OutputIconTick = "tick" + OutputIconCross = "cross" + OutputIconPipe = "pipe" +) + +type AwsIdentityResult struct { + AccountID string + UserID string +} + +func getAwsIdentity(ctx context.Context, cfg aws.Config) (*AwsIdentityResult, error) { + res, err := sts.NewFromConfig(cfg).GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) + if err != nil { + log("%s error checking AWS auth: %s", IconCross, err.Error()) + return nil, err + } + + return &AwsIdentityResult{ + AccountID: *res.Account, + UserID: *res.UserId, + }, nil +} + +type LoadKeelConfigArgs struct { + ProjectRoot string + Env string +} + +func loadKeelConfig(args *LoadKeelConfigArgs) (*config.ProjectConfig, error) { + t := NewTiming() + + configFiles, err := config.LoadAll(args.ProjectRoot) + if err != nil { + log("%s error loading Keel config: %s", IconCross, err.Error()) + return nil, err + } + + var baseConfig *config.ConfigFile + var envConfig *config.ConfigFile + + for _, c := range configFiles { + if c.Env == "" { + baseConfig = c + } + if c.Env == args.Env { + envConfig = c + } + } + + // TODO: I'm not totally sure this is the right logic, but basically we prefer an env + // specific config file and then if that doesn't exists will accept a "base" config + // file e.g. keelconfig.yaml + c := envConfig + if c == nil { + c = baseConfig + } + + // If there is no config we return an empty one rather than error-ing here, this is + // because this function can be used outside of deploying to just do a build e.g. + // for `keel run`, `keel test` or the integration tests + if c == nil { + return &config.ProjectConfig{}, nil + } + + if c.Errors != nil { + log("%s error found in %s\n\n%s", IconCross, c.Filename, c.Errors.Error()) + return nil, c.Errors + } + + log("%s Using %s %s", IconTick, orange(c.Filename), t.Since()) + return c.Config, nil +} + +type PulumiConfig struct { + StackName string + WorkspaceOptions []auto.LocalWorkspaceOption +} + +type SetupPulumiArgs struct { + AwsConfig aws.Config + Config *config.ProjectConfig + Env string +} + +func setupPulumi(ctx context.Context, args *SetupPulumiArgs) (*PulumiConfig, error) { + bucketNameKey := fmt.Sprintf("/keel/%s/pulumi-bucket-name", args.Config.Deploy.ProjectName) + passphraseKey := fmt.Sprintf("/keel/%s/pulumi-passphrase", args.Config.Deploy.ProjectName) + + bucketName := "" + passphrase := "" + + result := &PulumiConfig{ + StackName: fmt.Sprintf("keel-%s-%s", args.Config.Deploy.ProjectName, args.Env), + } + + getParamsOut, err := ssm.NewFromConfig(args.AwsConfig).GetParameters(ctx, &ssm.GetParametersInput{ + Names: []string{ + bucketNameKey, + passphraseKey, + }, + WithDecryption: aws.Bool(true), + }) + if err != nil { + log("%s error getting deploy config from SSM: %s", IconCross, err.Error()) + return nil, err + } + + for _, p := range getParamsOut.Parameters { + if p.Name == nil { + continue + } + if *p.Name == bucketNameKey { + bucketName = *p.Value + } + if *p.Name == passphraseKey { + passphrase = *p.Value + } + } + + if bucketName == "" { + t := NewTiming() + v, err := randomString(hex.EncodeToString) + if err != nil { + log("%s error generating Pulumi state bucket name: %s", IconCross, err.Error()) + return nil, err + } + + // Very annoying bit of the AWS SDK. If the region is us-east-1, then CreateBucketConfiguration must be set to nil + var bucketConfig *types.CreateBucketConfiguration + if args.Config.Deploy.Region != "us-east-1" { + bucketConfig = &types.CreateBucketConfiguration{LocationConstraint: types.BucketLocationConstraint(args.Config.Deploy.Region)} + } + + bucketName = fmt.Sprintf("keel-%s-%s", args.Config.Deploy.ProjectName, strings.ToLower(v[:7])) + _, err = s3.NewFromConfig(args.AwsConfig).CreateBucket(ctx, &s3.CreateBucketInput{ + Bucket: aws.String(bucketName), + CreateBucketConfiguration: bucketConfig, + }) + if err != nil { + log("%s error creating Pulumi state bucket: %s", IconCross, err.Error()) + return nil, err + } + + _, err = ssm.NewFromConfig(args.AwsConfig).PutParameter(ctx, &ssm.PutParameterInput{ + Name: &bucketNameKey, + Value: aws.String(bucketName), + Type: ssmtypes.ParameterTypeString, + }) + if err != nil { + log("%s error setting Pulumi state bucket name in SSM: %s", IconCross, err.Error()) + return nil, err + } + + log("%s Pulumi state bucket created %s", IconTick, t.Since()) + } + + if passphrase == "" { + t := NewTiming() + value, err := randomString(base64.StdEncoding.EncodeToString) + if err != nil { + log("%s error generating Pulumi passphrase: %s", IconCross, err.Error()) + return nil, err + } + + passphrase = value + _, err = ssm.NewFromConfig(args.AwsConfig).PutParameter(ctx, &ssm.PutParameterInput{ + Name: &passphraseKey, + Value: aws.String(value), + Type: ssmtypes.ParameterTypeSecureString, + }) + if err != nil { + log("%s error setting Pulumi passphrase in SSM: %s", IconCross, err.Error()) + return nil, err + } + + log("%s Pulumi passphrase created %s", IconTick, t.Since()) + } + + t := NewTiming() + pulumiCmd, err := auto.InstallPulumiCommand(ctx, &auto.PulumiCommandOptions{ + SkipVersionCheck: true, + }) + if err != nil { + log("%s error installing Pulumi: %s", IconCross, err.Error()) + return nil, err + } + + log("%s Pulumi version %s installed %s", IconTick, orange(pulumiCmd.Version().String()), t.Since()) + + result.WorkspaceOptions = []auto.LocalWorkspaceOption{ + auto.Pulumi(pulumiCmd), + auto.Project(workspace.Project{ + Name: tokens.PackageName(args.Config.Deploy.ProjectName), + Runtime: workspace.NewProjectRuntimeInfo("go", nil), + Backend: &workspace.ProjectBackend{ + URL: fmt.Sprintf("s3://%s", bucketName), + }, + }), + auto.EnvVars( + map[string]string{ + "PULUMI_CONFIG_PASSPHRASE": passphrase, + }, + ), + } + + return result, nil +} + +type CreatePrivateKeySecretArgs struct { + AwsConfig aws.Config + ProjectName string + Env string +} + +func createPrivateKeySecret(ctx context.Context, args *CreatePrivateKeySecretArgs) error { + ssmClient := ssm.NewFromConfig(args.AwsConfig) + + privateKeyParamName := runtime.SsmParameterName(args.ProjectName, args.Env, "KEEL_PRIVATE_KEY") + + getParamResult, err := ssmClient.GetParameter(ctx, &ssm.GetParameterInput{ + Name: aws.String(privateKeyParamName), + }) + if err != nil { + var ae smithy.APIError + if !errors.As(err, &ae) || ae.ErrorCode() != "ParameterNotFound" { + log("%s error fetching private key secret from SSM: %s", IconCross, err.Error()) + return err + } + } + + if getParamResult != nil && getParamResult.Parameter != nil { + log("%s Using existing private key", IconTick) + return nil + } + + t := Timing{} + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + log("%s error generating private key: %s", IconCross, err.Error()) + return err + } + + privateKeyPem := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + }) + + _, err = ssmClient.PutParameter(ctx, &ssm.PutParameterInput{ + Name: aws.String(privateKeyParamName), + Value: aws.String(string(privateKeyPem)), + Type: ssmtypes.ParameterTypeSecureString, + }) + if err != nil { + log("%s error setting private key secret in SSM: %s", IconCross, err.Error()) + return err + } + + log("%s New private key created %s", IconTick, t.Since()) + return nil +} + +func randomString(encode func([]byte) string) (string, error) { + bytes := make([]byte, 32) + _, err := rand.Read(bytes) + if err != nil { + return "", err + } + return encode(bytes), nil +} diff --git a/deploy/stack.go b/deploy/stack.go new file mode 100644 index 000000000..fb9943a09 --- /dev/null +++ b/deploy/stack.go @@ -0,0 +1,145 @@ +package deploy + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/pulumi/pulumi/sdk/v3/go/auto" + "github.com/teamkeel/keel/config" + "github.com/teamkeel/keel/proto" +) + +type SelectStackArgs struct { + AwsConfig aws.Config + PulumiConfig *PulumiConfig + Env string + Schema *proto.Schema + Config *config.ProjectConfig + AccountID string + RuntimeLambdaPath string + FunctionsLambdaPath string +} + +func selectStack(ctx context.Context, args *SelectStackArgs) (auto.Stack, error) { + runFunc := createProgram(&NewProgramArgs{ + AwsConfig: args.AwsConfig, + AwsAccountID: args.AccountID, + RuntimeLambdaPath: args.RuntimeLambdaPath, + FunctionsLambdaPath: args.FunctionsLambdaPath, + Env: args.Env, + Config: args.Config, + Schema: args.Schema, + }) + + t := NewTiming() + s, err := auto.UpsertStackInlineSource( + ctx, + args.PulumiConfig.StackName, + args.Config.Deploy.ProjectName, + runFunc, + args.PulumiConfig.WorkspaceOptions...) + if err != nil { + log("%s error selecting stack: %s", IconCross, err.Error()) + return s, err + } + + log("%s Selected stack %s %s", IconTick, orange(args.PulumiConfig.StackName), t.Since()) + + awsPluginVersion := "v6.63.0" + + // for inline source programs, we must manage plugins ourselves + err = s.Workspace().InstallPlugin(ctx, "aws", awsPluginVersion) + if err != nil { + log("%s error installing AWS Pulumi plugin:%s", IconCross, err.Error()) + return s, err + } + + log("%s AWS plugin %s installed %s", IconTick, orange(awsPluginVersion), t.Since()) + + // set stack configuration specifying the AWS region to deploy + err = s.SetConfig(ctx, "aws:region", auto.ConfigValue{Value: args.Config.Deploy.Region}) + if err != nil { + log("%s error setting aws:region on Pulumi stack: %s", IconCross, err.Error()) + return s, err + } + + _, err = s.Refresh(ctx) + if err != nil { + log("%s error refreshing stack: %s", IconCross, err.Error()) + return s, err + } + + log("%s Stack refreshed %s", IconTick, t.Since()) + return s, nil +} + +const ( + StackOutputApiURL = "apiUrl" + StackOutputDatabaseEndpoint = "databaseEndpoint" + StackOutputDatabaseDbName = "databaseDbName" + StackOutputDatabaseSecretArn = "databaseSecretArn" + StackOutputApiLambdaName = "apiLambdaName" + StackOutputSubscriberLambdaName = "subscriberLambdaName" + StackOutputJobsLambdaName = "jobsLambdaName" + StackOutputFunctionsLambdaName = "functionsLambdaName" +) + +type StackOutputs struct { + ApiURL string + DatabaseEndpoint string + DatabaseDbName string + DatabaseSecretArn string + ApiLambdaName string + SubscriberLambdaName string + JobsLambdaName string + FunctionsLambdaName string +} + +func getStackOutputs(ctx context.Context, pulumiConfig *PulumiConfig) (*StackOutputs, error) { + ws, err := auto.NewLocalWorkspace(ctx, pulumiConfig.WorkspaceOptions...) + if err != nil { + return nil, err + } + + stack, err := auto.SelectStack(ctx, pulumiConfig.StackName, ws) + if auto.IsSelectStack404Error(err) { + return nil, nil + } + if err != nil { + return nil, err + } + + outputs, err := stack.Outputs(ctx) + if err != nil { + return nil, err + } + + return parseStackOutputs(outputs), nil +} + +func parseStackOutputs(outputs auto.OutputMap) *StackOutputs { + result := &StackOutputs{} + for key, output := range outputs { + v := output.Value.(string) + switch key { + case StackOutputApiURL: + result.ApiURL = v + case StackOutputDatabaseDbName: + result.DatabaseDbName = v + case StackOutputDatabaseEndpoint: + result.DatabaseEndpoint = v + case StackOutputDatabaseSecretArn: + result.DatabaseSecretArn = v + case StackOutputApiLambdaName: + result.ApiLambdaName = v + case StackOutputSubscriberLambdaName: + result.SubscriberLambdaName = v + case StackOutputJobsLambdaName: + result.JobsLambdaName = v + case StackOutputFunctionsLambdaName: + result.FunctionsLambdaName = v + } + } + + return result +} diff --git a/go.mod b/go.mod index 71f560818..8159c0a8e 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,37 @@ module github.com/teamkeel/keel -go 1.23 +go 1.22 + +toolchain go1.23.2 require ( github.com/99designs/gqlgen v0.17.16 github.com/Masterminds/semver/v3 v3.2.1 github.com/PaesslerAG/jsonpath v0.1.1 + github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 github.com/alecthomas/participle/v2 v2.0.0-beta.5 + github.com/aws/aws-lambda-go v1.47.0 + github.com/aws/aws-sdk-go v1.44.298 + github.com/aws/aws-sdk-go-v2 v1.32.6 + github.com/aws/aws-sdk-go-v2/config v1.27.23 + github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.43.2 + github.com/aws/aws-sdk-go-v2/service/lambda v1.65.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.65.2 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.2 + github.com/aws/aws-sdk-go-v2/service/sqs v1.37.0 + github.com/aws/aws-sdk-go-v2/service/ssm v1.55.2 + github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 + github.com/aws/smithy-go v1.22.1 github.com/bmatcuk/doublestar/v4 v4.2.0 github.com/bykof/gostradamus v1.0.4 github.com/charmbracelet/bubbles v0.16.1 - github.com/charmbracelet/bubbletea v0.24.2 - github.com/charmbracelet/lipgloss v0.7.1 + github.com/charmbracelet/bubbletea v0.25.0 + github.com/charmbracelet/lipgloss v0.10.0 github.com/coreos/go-oidc v2.2.1+incompatible github.com/dchest/uniuri v1.2.0 github.com/docker/docker v24.0.9+incompatible github.com/docker/go-connections v0.4.0 + github.com/evanw/esbuild v0.24.0 github.com/fatih/camelcase v1.0.0 github.com/goccy/go-yaml v1.12.0 github.com/golang-jwt/jwt/v4 v4.4.2 @@ -31,87 +47,154 @@ require ( github.com/nleeper/goment v1.4.4 github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 github.com/otiai10/copy v1.7.0 + github.com/pulumi/pulumi-aws/sdk/v6 v6.55.0 + github.com/pulumi/pulumi/sdk/v3 v3.136.1 github.com/radovskyb/watcher v1.0.7 github.com/relvacode/iso8601 v1.3.0 github.com/rs/cors v1.8.2 - github.com/samber/lo v1.28.0 + github.com/samber/lo v1.47.0 github.com/sanity-io/litter v1.5.5 github.com/segmentio/ksuid v1.0.4 - github.com/sergi/go-diff v1.2.0 - github.com/spf13/cobra v1.5.0 + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 + github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.15.0 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/teamkeel/graphql v0.8.2-0.20230531102419-995b8ab035b6 github.com/twitchtv/twirp v8.1.3+incompatible github.com/vincent-petithory/dataurl v1.0.0 github.com/xeipuuv/gojsonschema v1.2.0 - go.opentelemetry.io/otel v1.21.0 + go.opentelemetry.io/otel v1.31.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 - go.opentelemetry.io/otel/sdk v1.21.0 - go.opentelemetry.io/otel/trace v1.21.0 + go.opentelemetry.io/otel/sdk v1.31.0 + go.opentelemetry.io/otel/trace v1.31.0 go.opentelemetry.io/proto/otlp v1.0.0 - golang.org/x/exp v0.0.0-20220907003533-145caa8ea1d0 - golang.org/x/oauth2 v0.13.0 - google.golang.org/protobuf v1.33.0 + golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 + golang.org/x/oauth2 v0.17.0 + golang.org/x/sync v0.7.0 + google.golang.org/protobuf v1.34.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.5.0 gorm.io/gorm v1.25.1 ) require ( + dario.cat/mergo v1.0.0 // indirect + github.com/BurntSushi/toml v1.2.1 // indirect github.com/PaesslerAG/gval v1.0.0 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.23 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cheggaaa/pb v1.0.29 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect + github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect - github.com/fatih/color v1.13.0 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/djherbis/times v1.5.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-logr/logr v1.3.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-git/go-git/v5 v5.12.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/glog v1.2.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/hcl/v2 v2.17.0 // indirect + github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.1 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/nxadm/tail v1.4.11 // indirect + github.com/opentracing/basictracer-go v1.1.0 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/pgavlin/fx v0.1.6 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/term v1.1.0 // indirect github.com/pquerna/cachecontrol v0.2.0 // indirect - github.com/rivo/uniseg v0.4.4 // indirect + github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect + github.com/pulumi/esc v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect + github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect github.com/spf13/afero v1.9.3 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect + github.com/texttheater/golang-levenshtein v1.0.1 // indirect github.com/tkuchiki/go-timezone v0.2.0 // indirect + github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect + github.com/uber/jaeger-lib v2.4.1+incompatible // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - go.opentelemetry.io/otel/metric v1.21.0 // indirect - golang.org/x/sync v0.3.0 // indirect - golang.org/x/term v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect + github.com/zclconf/go-cty v1.13.2 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/term v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.22.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect - google.golang.org/grpc v1.59.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7 // indirect + google.golang.org/grpc v1.63.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + lukechampine.com/frand v1.4.2 // indirect ) require ( - github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -125,8 +208,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sirupsen/logrus v1.9.0 github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/crypto v0.17.0 - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/crypto v0.24.0 + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.26.0 // indirect gotest.tools/v3 v3.3.0 // indirect ) diff --git a/go.sum b/go.sum index 76ba43a8a..dbd7021a2 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/99designs/gqlgen v0.17.16 h1:tTIw/cQ/uvf3iXIb2I6YSkdaDkmHmH2W2eZkVe0IVLA= github.com/99designs/gqlgen v0.17.16/go.mod h1:dnJdUkgfh8iw8CEx2hhTdgTQO/GvVWKLcm/kult5gwI= @@ -42,16 +44,29 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8= github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I= github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk= github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= @@ -63,12 +78,73 @@ github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0q github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI= +github.com/aws/aws-lambda-go v1.47.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= +github.com/aws/aws-sdk-go v1.44.298 h1:5qTxdubgV7PptZJmp/2qDwD2JL187ePL7VOxsSh1i3g= +github.com/aws/aws-sdk-go v1.44.298/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go-v2 v1.32.6 h1:7BokKRgRPuGmKkFMhEg/jSul+tB9VvXhcViILtfG8b4= +github.com/aws/aws-sdk-go-v2 v1.32.6/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 h1:pT3hpW0cOHRJx8Y0DfJUEQuqPild8jRGmSFmBgvydr0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6/go.mod h1:j/I2++U0xX+cr44QjHay4Cvxj6FUbnxrgmqN3H1jTZA= +github.com/aws/aws-sdk-go-v2/config v1.27.23 h1:Cr/gJEa9NAS7CDAjbnB7tHYb3aLZI2gVggfmSAasDac= +github.com/aws/aws-sdk-go-v2/config v1.27.23/go.mod h1:WMMYHqLCFu5LH05mFOF5tsq1PGEMfKbu083VKqLCd0o= +github.com/aws/aws-sdk-go-v2/credentials v1.17.23 h1:G1CfmLVoO2TdQ8z9dW+JBc/r8+MqyPQhXCafNZcXVZo= +github.com/aws/aws-sdk-go-v2/credentials v1.17.23/go.mod h1:V/DvSURn6kKgcuKEk4qwSwb/fZ2d++FFARtWSbXnLqY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9 h1:Aznqksmd6Rfv2HQN9cpqIV/lQRMaIpJkLLaJ1ZI76no= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9/go.mod h1:WQr3MY7AxGNxaqAtsDWn+fBxmd4XvLkzeqQ8P1VM0/w= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 h1:s/fF4+yDQDoElYhfIVvSNyeCydfbuTKzhxSXDXCPasU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25/go.mod h1:IgPfDv5jqFIzQSNbUEMoitNooSMXjRSDkhXv8jiROvU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 h1:ZntTCl5EsYnhN/IygQEUugpdwbhdkom9uHcbCftiGgA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25/go.mod h1:DBdPrgeocww+CSl1C8cEV8PN1mHMBhuCDLpXezyvWkE= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21 h1:7edmS3VOBDhK00b/MwGtGglCm7hhwNYnjJs/PgFdMQE= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21/go.mod h1:Q9o5h4HoIWG8XfzxqiuK/CGUbepCJ8uTlaE3bAbxytQ= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.43.2 h1:QaFEWSbTr3n31uaRyMPX2wCuzUGIS+VYM1xv5+I2FRo= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.43.2/go.mod h1:dLKWdVHc4B1v+N6SLYkCUQjE4urPT4abG98sHbR5jnw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2 h1:4FMHqLfk0efmTqhXVRL5xYRqlEBNBiRI7N6w4jsEdd4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2/go.mod h1:LWoqeWlK9OZeJxsROW2RqrSPvQHKTpp69r/iDjwsSaw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2/go.mod h1:fnjjWyAW/Pj5HYOxl9LJqWtEwS7W2qgcRLWP+uWbss0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2 h1:t7iUP9+4wdc5lt3E41huP+GvQZJD38WLsgVp4iOtAjg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2/go.mod h1:/niFCtmuQNxqx9v8WAPq5qh7EH25U4BF6tjoyq9bObM= +github.com/aws/aws-sdk-go-v2/service/lambda v1.65.0 h1:c4eYRkhqXsyoIQ4Z8e3E1fBmxOB3XnAfbYw0x+kyHdw= +github.com/aws/aws-sdk-go-v2/service/lambda v1.65.0/go.mod h1:4L6vIpiChdahncljlDFzKWGiZsLgszGwDoYqMDhb6T4= +github.com/aws/aws-sdk-go-v2/service/s3 v1.65.2 h1:yi8m+jepdp6foK14xXLGkYBenxnlcfJ45ka4Pg7fDSQ= +github.com/aws/aws-sdk-go-v2/service/s3 v1.65.2/go.mod h1:cB6oAuus7YXRZhWCc1wIwPywwZ1XwweNp2TVAEGYeB8= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.2 h1:Rrqru2wYkKQCS2IM5/JrgKUQIoNTqA6y/iuxkjzxC6M= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.2/go.mod h1:QuCURO98Sqee2AXmqDNxKXYFm2OEDAVAPApMqO0Vqnc= +github.com/aws/aws-sdk-go-v2/service/sqs v1.37.0 h1:4el/8jdTeg0Rx/ws3yIEPXR1LfSUiMKhdb/WuDwKzKI= +github.com/aws/aws-sdk-go-v2/service/sqs v1.37.0/go.mod h1:YXj6Y1BjZNj1PKi78CX2hBkVpCCuJ0TRtyd6wrKVQ64= +github.com/aws/aws-sdk-go-v2/service/ssm v1.55.2 h1:z6Pq4+jtKlhK4wWJGHRGwMLGjC1HZwAO3KJr/Na0tSU= +github.com/aws/aws-sdk-go-v2/service/ssm v1.55.2/go.mod h1:DSmu/VZzpQlAubWBbAvNpt+S4k/XweglJi4XaDGyvQk= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 h1:p1GahKIjyMDZtiKoIn0/jAj/TkMzfzndDv5+zi2Mhgc= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.1/go.mod h1:/vWdhoIoYA5hYoPZ6fm7Sv4d8701PiG5VKe8/pPJL60= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.1 h1:lCEv9f8f+zJ8kcFeAjRZsekLd/x5SAm96Cva+VbUdo8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.1/go.mod h1:xyFHA4zGxgYkdD73VeezHt3vSKEG9EmFnGwoKlP00u4= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 h1:CiS7i0+FUe+/YY1GvIBLLrR/XNGZ4CtM1Ll0XavNuVo= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.2/go.mod h1:HtaiBI8CjYoNVde8arShXb94UbQQi9L4EMr6D+xGBwo= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bmatcuk/doublestar/v4 v4.2.0 h1:Qu+u9wR3Vd89LnlLMHvnZ5coJMWKQamqdz9/p5GNthA= github.com/bmatcuk/doublestar/v4 v4.2.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bykof/gostradamus v1.0.4 h1:77iq/tANg5rZSxjoZ98zepZbv3VrotijEmlnH/WycD4= github.com/bykof/gostradamus v1.0.4/go.mod h1:pdH0bv8yFLwr4G6EbM1j3QUb4AdCmiG7xlTjVwYyPNM= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= @@ -76,10 +152,12 @@ github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= -github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= -github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= -github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= -github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= +github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= +github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= +github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo= +github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= @@ -87,6 +165,9 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -95,9 +176,11 @@ github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:Yyn github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -107,6 +190,8 @@ github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4 github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dgryski/trifles v0.0.0-20200830180326-aaf60a07f6a3 h1:JibukGTEjdN4VMX7YHmXQsLr/gPURUbetlH4E6KvHSU= github.com/dgryski/trifles v0.0.0-20200830180326-aaf60a07f6a3/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/djherbis/times v1.5.0 h1:79myA211VwPhFTqUk8xehWrsEO+zcIZj0zT8mXPVARU= +github.com/djherbis/times v1.5.0/go.mod h1:5q7FDLvbNg1L/KaBmPcWlVR9NmoKo3+ucqUA3ijQhA0= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= @@ -115,27 +200,44 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanw/esbuild v0.24.0 h1:GZ78naTLp7FKr+K7eNuM/SLs5maeiHYRPsTg6kmdsSE= +github.com/evanw/esbuild v0.24.0/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= @@ -146,16 +248,19 @@ github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7a github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM= github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= -github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= +github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -178,8 +283,9 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -209,12 +315,21 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -222,15 +337,18 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= +github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8= +github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -241,10 +359,16 @@ github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiw github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jonbretman/gotestpretty v0.0.0-20200908080245-691cf3e7550c h1:1+Edq3YG954jgmyg++u4ga1wimqNwQvlxuOjkAPYjgo= github.com/jonbretman/gotestpretty v0.0.0-20200908080245-691cf3e7550c/go.mod h1:XGJ4Wo9sjsEIW3FBp9tlKF98nH40an1qh7dt0Nc+DsA= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -253,7 +377,10 @@ github.com/ka-weihe/fast-levenshtein v0.0.0-20201227151214-4c99ee36a1ba h1:keZ4v github.com/ka-weihe/fast-levenshtein v0.0.0-20201227151214-4c99ee36a1ba/go.mod h1:kaXTPU4xitQT0rfT7/i9O9Gm8acSh3DXr0p4y3vKqiE= github.com/karlseguin/typed v1.1.8 h1:ND0eDpwiUFIrm/n1ehxUyh/XNGs9zkYrLxtGqENSalY= github.com/karlseguin/typed v1.1.8/go.mod h1:pZlmYaWQ7MVpwfIOP88fASh3LopVxKeE+uNXW3hQ2D8= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= @@ -279,21 +406,26 @@ github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYt github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -301,22 +433,31 @@ github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQB github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= -github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/nleeper/goment v1.4.4 h1:GlMTpxvhueljArSunzYjN9Ri4SOmpn0Vh2hg2z/IIl8= github.com/nleeper/goment v1.4.4/go.mod h1:zDl5bAyDhqxwQKAvkSXMRLOdCowrdZz53ofRJc4VhTo= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opentracing/basictracer-go v1.1.0 h1:Oa1fTSBvAl8pa3U+IJYqrKm0NALwH9OsgwOqDv4xJW0= +github.com/opentracing/basictracer-go v1.1.0/go.mod h1:V2HZueSJEp879yv285Aap1BS69fQMD+MNP1mRs6mBQc= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE= github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= @@ -326,49 +467,69 @@ github.com/otiai10/mint v1.3.3 h1:7JgpsBaN0uMkyju4tbYHu0mnM55hNKVYLsXmwr15NQI= github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pgavlin/fx v0.1.6 h1:r9jEg69DhNoCd3Xh0+5mIbdbS3PqWrVWujkY76MFRTU= +github.com/pgavlin/fx v0.1.6/go.mod h1:KWZJ6fqBBSh8GxHYqwYCf3rYE7Gp2p0N8tJp8xv9u9M= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= +github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/cachecontrol v0.2.0 h1:vBXSNuE5MYP9IJ5kjsdo8uq+w41jSPgvba2DEnkRx9k= github.com/pquerna/cachecontrol v0.2.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435cARxCW6q9gc0S/Yxz7Mkd38pOb0= +github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE= +github.com/pulumi/esc v0.9.1 h1:HH5eEv8sgyxSpY5a8yePyqFXzA8cvBvapfH8457+mIs= +github.com/pulumi/esc v0.9.1/go.mod h1:oEJ6bOsjYlQUpjf70GiX+CXn3VBmpwFDxUTlmtUN84c= +github.com/pulumi/pulumi-aws/sdk/v6 v6.55.0 h1:AMcQQvRantSQpO9oIwTvtg8Mm0MpniUMnizEl8Jee38= +github.com/pulumi/pulumi-aws/sdk/v6 v6.55.0/go.mod h1:HWyVOgw2WogCRYxH6eRSKM7fNK+vHXxPKqrbx/oy0wI= +github.com/pulumi/pulumi/sdk/v3 v3.136.1 h1:VJWTgdBrLvvzIkMbGq/epNEfT65P9gTvw14UF/I7hTI= +github.com/pulumi/pulumi/sdk/v3 v3.136.1/go.mod h1:PvKsX88co8XuwuPdzolMvew5lZV+4JmZfkeSjj7A6dI= github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE= github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= github.com/relvacode/iso8601 v1.3.0 h1:HguUjsGpIMh/zsTczGN3DVJFxTU/GX+MMmzcKoMO7ko= github.com/relvacode/iso8601 v1.3.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/samber/lo v1.28.0 h1:rsMtAXNgLuF0Gv3L0ypvm5RsN/vRO0m5pak9uiRb6NU= -github.com/samber/lo v1.28.0/go.mod h1:it33p9UtPMS7z72fP4gw/EIfQB2eI8ke7GR2wc6+Rhg= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= +github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE= +github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -379,6 +540,8 @@ github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jH github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -389,22 +552,28 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/teamkeel/graphql v0.8.2-0.20230531102419-995b8ab035b6 h1:q8ZbAgqr7jJlZNJ4WAI+QMuZrcCBDOw9k7orYuy+Vqs= github.com/teamkeel/graphql v0.8.2-0.20230531102419-995b8ab035b6/go.mod h1:5td34OA5ZUdckc2w3GgE7QQoaG8MK6hIVR3dFI+qaK4= -github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= -github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= +github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= github.com/tkuchiki/go-timezone v0.2.0 h1:yyZVHtQRVZ+wvlte5HXvSpBkR0dPYnPEIgq9qqAqltk= github.com/tkuchiki/go-timezone v0.2.0/go.mod h1:b1Ean9v2UXtxSq4TZF0i/TU9NuoWa9hOzOKoGCV2zqY= github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU= github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= +github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY= github.com/vektah/gqlparser/v2 v2.5.0/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs= github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI= github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= @@ -418,26 +587,30 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= +github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= -go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= -go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= -go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= -go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= -go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -446,9 +619,12 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -459,8 +635,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20220907003533-145caa8ea1d0 h1:17k44ji3KFYG94XS5QEFC8pyuOlMh3IoR+vkmTZmJJs= -golang.org/x/exp v0.0.0-20220907003533-145caa8ea1d0/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= +golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -486,6 +662,9 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -505,6 +684,7 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -518,10 +698,14 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -531,8 +715,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= +golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -546,8 +730,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -558,13 +742,13 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -578,6 +762,7 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -598,15 +783,20 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -615,15 +805,19 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -675,6 +869,9 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -706,8 +903,9 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -744,12 +942,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= -google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= -google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= -google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7 h1:8EeVk1VKMD+GD/neyEHGmz7pFblqPjHoi+PGQIlLx2s= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -766,8 +964,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -781,8 +979,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= +google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -793,9 +991,14 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -815,6 +1018,10 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= +lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= +pgregory.net/rapid v0.6.1 h1:4eyrDxyht86tT4Ztm+kvlyNBLIk071gR+ZQdhphc9dQ= +pgregory.net/rapid v0.6.1/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/integration/integration_test.go b/integration/integration_test.go index 48c8d65d1..09b6aface 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -35,7 +35,11 @@ func TestIntegration(t *gotest.T) { wd, err := os.Getwd() require.NoError(t, err) - err = node.Bootstrap(tmpDir, node.WithPackagesPath(filepath.Join(wd, "../packages")), node.WithLogger(func(s string) {})) + err = node.Bootstrap( + tmpDir, + node.WithPackagesPath(filepath.Join(wd, "../packages")), + node.WithLogger(func(s string) {}), + ) require.NoError(t, err) _, err = testhelpers.NpmInstall(tmpDir) diff --git a/integration/testdata/audit_logs/tests.test.ts b/integration/testdata/audit_logs/tests.test.ts index 96011f754..c00bab36f 100644 --- a/integration/testdata/audit_logs/tests.test.ts +++ b/integration/testdata/audit_logs/tests.test.ts @@ -585,9 +585,7 @@ test("job function with error and no rollback - audit table is not rolled back", weddingId: wedding.id, }); - await expect( - jobs.withIdentity(identity).updateHeadCount({ weddingId: wedding.id }) - ).toHaveError({ message: "prisma is not invited!" }); + await jobs.withIdentity(identity).updateHeadCount({ weddingId: wedding.id }); const inviteesAudits = await sql< Audit diff --git a/integration/testdata/events_basic/tests.test.ts b/integration/testdata/events_basic/tests.test.ts index b1a315851..5e5dc9d98 100644 --- a/integration/testdata/events_basic/tests.test.ts +++ b/integration/testdata/events_basic/tests.test.ts @@ -66,9 +66,7 @@ test("events from failed hook function with rollback", async () => { }); test("events from failed job", async () => { - await expect(jobs.createRandomPersons({ raiseException: true })).toHaveError({ - code: "ERR_UNKNOWN", - }); + await jobs.createRandomPersons({ raiseException: true }); const persons = await models.person.findMany(); diff --git a/integration/testdata/files/tests.test.ts b/integration/testdata/files/tests.test.ts index ff495248a..3ce2d1ffd 100644 --- a/integration/testdata/files/tests.test.ts +++ b/integration/testdata/files/tests.test.ts @@ -1,7 +1,6 @@ import { actions, resetDatabase, models } from "@teamkeel/testing"; import { beforeEach, expect, test } from "vitest"; -import { useDatabase, InlineFile, File } from "@teamkeel/sdk"; -import { sql } from "kysely"; +import { InlineFile, File } from "@teamkeel/sdk"; interface DbFile { id: string; @@ -30,23 +29,10 @@ test("files - create action with file input", async () => { const contents1 = await result.file?.read(); expect(contents1?.toString("utf-8")).toEqual("hello"); - const myfiles = await useDatabase() - .selectFrom("my_file") - .selectAll() - .execute(); + const myfile = await models.myFile.findOne({ id: result.id }); - const files = await sql`SELECT * FROM keel_storage`.execute( - useDatabase() - ); - - expect(myfiles.length).toEqual(1); - expect(files.rows.length).toEqual(1); - expect(files.rows[0].id).toEqual(myfiles[0].file?.key); - expect(files.rows[0].filename).toEqual(myfiles[0].file?.filename); - expect(files.rows[0].contentType).toEqual(myfiles[0].file?.contentType); - - const contents = files.rows[0].data.toString("utf-8"); - expect(contents).toEqual("hello"); + expect(myfile?.file?.contentType).toEqual("text/plain"); + expect(myfile?.file?.filename).toEqual("my-file.txt"); }); test("files - update action with file input", async () => { @@ -80,23 +66,10 @@ test("files - update action with file input", async () => { const contents1 = await updated.file?.read(); expect(contents1?.toString("utf-8")).toEqual("hello again"); - const myfiles = await useDatabase() - .selectFrom("my_file") - .selectAll() - .execute(); - - const files = - await sql`SELECT * FROM keel_storage ORDER BY created_at DESC`.execute( - useDatabase() - ); - + const myfiles = await models.myFile.findMany(); expect(myfiles.length).toEqual(1); - expect(files.rows.length).toEqual(2); - expect(files.rows[0].id).toEqual(myfiles[0].file?.key); - expect(files.rows[0].filename).toEqual(myfiles[0].file?.filename); - expect(files.rows[0].contentType).toEqual(myfiles[0].file?.contentType); - const contents = files.rows[0].data.toString("utf-8"); + const contents = (await myfiles[0].file?.read())?.toString("utf-8"); expect(contents).toEqual("hello again"); }); @@ -128,26 +101,18 @@ test("files - update action with file input and empty hooks", async () => { expect(updated.file?.filename).toEqual("my-second-file.txt"); expect(updated.file?.size).toEqual(11); + // key should have changed + expect(updated.file?.key).not.toEqual(result.file?.key); + const contents1 = await updated.file?.read(); expect(contents1?.toString("utf-8")).toEqual("hello again"); - const myfiles = await useDatabase() - .selectFrom("my_file") - .selectAll() - .execute(); - - const files = - await sql`SELECT * FROM keel_storage ORDER BY created_at DESC`.execute( - useDatabase() - ); - + const myfiles = await models.myFile.findMany(); expect(myfiles.length).toEqual(1); - expect(files.rows.length).toEqual(2); - expect(files.rows[0].id).toEqual(myfiles[0].file?.key); - expect(files.rows[0].filename).toEqual(myfiles[0].file?.filename); - expect(files.rows[0].contentType).toEqual(myfiles[0].file?.contentType); + expect(myfiles[0].id).toEqual(updated.id); + expect(myfiles[0].file?.filename).toEqual("my-second-file.txt"); - const contents = files.rows[0].data.toString("utf-8"); + const contents = (await myfiles[0].file?.read())?.toString("utf-8"); expect(contents).toEqual("hello again"); }); @@ -254,46 +219,20 @@ test("files - list action empty hooks", async () => { test("files - create file in hook", async () => { await actions.createFileInHook({}); - const myfiles = await useDatabase() - .selectFrom("my_file") - .selectAll() - .execute(); - - const files = - await sql`SELECT * FROM keel_storage ORDER BY created_at DESC`.execute( - useDatabase() - ); - + const myfiles = await models.myFile.findMany(); expect(myfiles.length).toEqual(1); - expect(files.rows.length).toEqual(1); - expect(files.rows[0].id).toEqual(myfiles[0].file?.key); - expect(files.rows[0].filename).toEqual(myfiles[0].file?.filename); - expect(files.rows[0].contentType).toEqual(myfiles[0].file?.contentType); - const contents = files.rows[0].data.toString("utf-8"); + const contents = (await myfiles[0].file?.read())?.toString("utf-8"); expect(contents).toEqual("created in hook!"); }); test("files - create and store file in hook", async () => { await actions.createFileAndStoreInHook({}); - const myfiles = await useDatabase() - .selectFrom("my_file") - .selectAll() - .execute(); - - const files = - await sql`SELECT * FROM keel_storage ORDER BY created_at DESC`.execute( - useDatabase() - ); - + const myfiles = await models.myFile.findMany(); expect(myfiles.length).toEqual(1); - expect(files.rows.length).toEqual(1); - expect(files.rows[0].id).toEqual(myfiles[0].file?.key); - expect(files.rows[0].filename).toEqual(myfiles[0].file?.filename); - expect(files.rows[0].contentType).toEqual(myfiles[0].file?.contentType); - const contents = files.rows[0].data.toString("utf-8"); + const contents = (await myfiles[0].file?.read())?.toString("utf-8"); expect(contents).toEqual("created and stored in hook!"); }); @@ -309,25 +248,12 @@ test("files - read and store in query hook", async () => { await actions.getFileNumerateContents({ id: result.id }); await actions.getFileNumerateContents({ id: result.id }); - await actions.getFileNumerateContents({ id: result.id }); - - const myfiles = await useDatabase() - .selectFrom("my_file") - .selectAll() - .execute(); - - const files = - await sql`SELECT * FROM keel_storage ORDER BY created_at DESC`.execute( - useDatabase() - ); + const res = await actions.getFileNumerateContents({ id: result.id }); + const myfiles = await models.myFile.findMany(); expect(myfiles.length).toEqual(1); - expect(files.rows.length).toEqual(1); - expect(files.rows[0].id).toEqual(myfiles[0].file?.key); - expect(files.rows[0].filename).toEqual(myfiles[0].file?.filename); - expect(files.rows[0].contentType).toEqual(myfiles[0].file?.contentType); - const contents = files.rows[0].data.toString("utf-8"); + const contents = (await myfiles[0].file?.read())?.toString("utf-8"); expect(contents).toEqual("4"); }); @@ -355,16 +281,7 @@ test("files - write many, store many", async () => { const keys = myfiles.map((a) => a.file!.key); keys.push((result.msg.file as File).key); - const files = - await sql`SELECT * FROM keel_storage ORDER BY created_at DESC`.execute( - useDatabase() - ); - - const fileIds = files.rows.map((a) => a.id); - expect(myfiles.length).toEqual(3); - expect(files.rows.length).toEqual(4); - expect(keys.sort()).toEqual(fileIds.sort()); }); test("files - store once, write many", async () => { @@ -384,22 +301,13 @@ test("files - store once, write many", async () => { const contents = await result.msg.file.read(); expect(contents.toString("utf-8")).toEqual("hello"); - const myfiles = await useDatabase() - .selectFrom("my_file") - .selectAll() - .execute(); - - const files = - await sql`SELECT * FROM keel_storage ORDER BY created_at DESC`.execute( - useDatabase() - ); + const myfiles = await models.myFile.findMany(); expect(myfiles.length).toEqual(3); - expect(files.rows.length).toEqual(1); - expect(myfiles[0].file?.key).toEqual(files.rows[0].id); - expect(myfiles[1].file?.key).toEqual(files.rows[0].id); - expect(myfiles[2].file?.key).toEqual(files.rows[0].id); - expect((result.msg.file as File).key).toEqual(files.rows[0].id); + + // all files should have the same file key + expect(myfiles[1].file?.key).toEqual(myfiles[0].file?.key); + expect(myfiles[2].file?.key).toEqual(myfiles[0].file?.key); }); test("files - model API file tests", async () => { @@ -419,6 +327,7 @@ test("files - presigned url", async () => { const result = await actions.presignedUrl({ file: InlineFile.fromDataURL(dataUrl), }); + const url = new URL(result); - expect(result).toEqual(dataUrl); + expect(url.searchParams.get("X-Amz-Algorithm")).toEqual("AWS4-HMAC-SHA256"); }); diff --git a/integration/testdata/jobs_permissions/tests.test.ts b/integration/testdata/jobs_permissions/tests.test.ts index 05587cfc4..41f5743d3 100644 --- a/integration/testdata/jobs_permissions/tests.test.ts +++ b/integration/testdata/jobs_permissions/tests.test.ts @@ -11,39 +11,32 @@ async function jobRan(id) { test("job - without identity - not permitted", async () => { const { id } = await models.trackJob.create({ didJobRun: false }); - await expect(jobs.manualJob({ id })).toHaveAuthorizationError(); + await jobs.manualJob({ id }); expect(await jobRan(id)).toBeFalsy(); - await expect( - jobs.manualJobIsAuthenticatedExpression({ id }) - ).toHaveAuthorizationError(); + await jobs.manualJobIsAuthenticatedExpression({ id }); + expect(await jobRan(id)).toBeFalsy(); - await expect(jobs.manualJobMultiRoles({ id })).toHaveAuthorizationError(); + await jobs.manualJobMultiRoles({ id }); expect(await jobRan(id)).toBeFalsy(); }); test("job - invalid token - not authenticated", async () => { const { id } = await models.trackJob.create({ didJobRun: false }); - await expect( - jobs.withAuthToken("invalid").manualJobTrueExpression({ id }) - ).not.toHaveAuthorizationError(); + await jobs.withAuthToken("invalid").manualJobTrueExpression({ id }); expect(await jobRan(id)).toBeFalsy(); - await expect( - jobs.withAuthToken("invalid").manualJob({ id }) - ).toHaveAuthenticationError(); + await jobs.withAuthToken("invalid").manualJob({ id }); expect(await jobRan(id)).toBeFalsy(); - await expect( - jobs.withAuthToken("invalid").manualJobIsAuthenticatedExpression({ id }) - ).toHaveAuthenticationError(); + await jobs + .withAuthToken("invalid") + .manualJobIsAuthenticatedExpression({ id }); expect(await jobRan(id)).toBeFalsy(); - await expect( - jobs.withAuthToken("invalid").manualJobMultiRoles({ id }) - ).toHaveAuthenticationError(); + await jobs.withAuthToken("invalid").manualJobMultiRoles({ id }); expect(await jobRan(id)).toBeFalsy(); }); @@ -51,9 +44,7 @@ test("job - with identity, ctx.isAuthenticated - permitted", async () => { const { id } = await models.trackJob.create({ didJobRun: false }); const identity = await models.identity.create({ email: "weave@gmail.com" }); - await expect( - jobs.withIdentity(identity).manualJobIsAuthenticatedExpression({ id }) - ).not.toHaveAuthorizationError(); + await jobs.withIdentity(identity).manualJobIsAuthenticatedExpression({ id }); expect(await jobRan(id)).toBeTruthy(); }); @@ -63,19 +54,14 @@ test("job - with token, ctx.isAuthenticated - permitted", async () => { email: "weave@gmail.com", }); - await expect( - jobs.withIdentity(identity).manualJobIsAuthenticatedExpression({ id }) - ).not.toHaveAuthorizationError(); + await jobs.withIdentity(identity).manualJobIsAuthenticatedExpression({ id }); expect(await jobRan(id)).toBeTruthy(); }); test("job - without identity, true expression permission - permitted", async () => { const { id } = await models.trackJob.create({ didJobRun: false }); - await expect( - jobs.manualJobTrueExpression({ id }) - ).not.toHaveAuthorizationError(); - + await jobs.manualJobTrueExpression({ id }); expect(await jobRan(id)).toBeTruthy(); }); @@ -86,16 +72,10 @@ test("job - wrong domain - not permitted", async () => { emailVerified: true, }); - await expect( - jobs.withIdentity(identity).manualJob({ id }) - ).toHaveAuthorizationError(); - + await jobs.withIdentity(identity).manualJob({ id }); expect(await jobRan(id)).toBeFalsy(); - await expect( - jobs.withIdentity(identity).manualJobMultiRoles({ id }) - ).toHaveAuthorizationError(); - + await jobs.withIdentity(identity).manualJobMultiRoles({ id }); expect(await jobRan(id)).toBeFalsy(); }); @@ -106,10 +86,7 @@ test("job - authorised domain - permitted", async () => { emailVerified: true, }); - await expect( - jobs.withIdentity(identity).manualJob({ id }) - ).not.toHaveAuthorizationError(); - + await jobs.withIdentity(identity).manualJob({ id }); expect(await jobRan(id)).toBeTruthy(); }); @@ -120,10 +97,7 @@ test("job - wrong authorised domain - not permitted", async () => { emailVerified: true, }); - await expect( - jobs.withIdentity(identity).manualJob({ id }) - ).toHaveAuthorizationError(); - + await jobs.withIdentity(identity).manualJob({ id }); expect(await jobRan(id)).toBeFalsy(); }); @@ -134,38 +108,28 @@ test("job - multi domains, authorised domain - permitted", async () => { emailVerified: true, }); - await expect( - jobs.withIdentity(identity).manualJobMultiRoles({ id }) - ).not.toHaveAuthorizationError(); - + await jobs.withIdentity(identity).manualJobMultiRoles({ id }); expect(await jobRan(id)).toBeTruthy(); }); test("job - true expression - permitted", async () => { const { id } = await models.trackJob.create({ didJobRun: false }); - await expect( - jobs.manualJobTrueExpression({ id }) - ).not.toHaveAuthorizationError(); - + await jobs.manualJobTrueExpression({ id }); expect(await jobRan(id)).toBeTruthy(); }); test("job - env var expression - permitted", async () => { const { id } = await models.trackJob.create({ didJobRun: false }); - await expect( - jobs.manualJobEnvExpression({ id }) - ).not.toHaveAuthorizationError(); - + await jobs.manualJobEnvExpression({ id }); expect(await jobRan(id)).toBeTruthy(); }); test("job - env var expression fail - not permitted", async () => { const { id } = await models.trackJob.create({ didJobRun: false }); - await expect(jobs.manualJobEnvExpression2({ id })).toHaveAuthorizationError(); - + await jobs.manualJobEnvExpression2({ id }); expect(await jobRan(id)).toBeFalsy(); }); @@ -176,10 +140,7 @@ test("job - multiple permissions - not permitted", async () => { emailVerified: true, }); - await expect( - jobs.withIdentity(identity).manualJobMultiPermission({ id }) - ).toHaveAuthorizationError(); - + await jobs.withIdentity(identity).manualJobMultiPermission({ id }); expect(await jobRan(id)).toBeFalsy(); }); @@ -190,10 +151,7 @@ test("job - multiple permissions - permitted", async () => { emailVerified: true, }); - await expect( - jobs.withIdentity(identity).manualJobMultiPermission({ id }) - ).not.toHaveAuthorizationError(); - + await jobs.withIdentity(identity).manualJobMultiPermission({ id }); expect(await jobRan(id)).toBeTruthy(); }); @@ -204,10 +162,9 @@ test("job - allowed in job code - permitted", async () => { emailVerified: true, }); - await expect( - jobs.withIdentity(identity).manualJobDeniedInCode({ id, denyIt: false }) - ).not.toHaveAuthorizationError(); - + await jobs + .withIdentity(identity) + .manualJobDeniedInCode({ id, denyIt: false }); expect(await jobRan(id)).toBeTruthy(); }); @@ -218,9 +175,7 @@ test("job - denied in job code - not permitted without rollback transaction", as emailVerified: true, }); - await expect( - jobs.withIdentity(identity).manualJobDeniedInCode({ id, denyIt: true }) - ).toHaveAuthorizationError(); + await jobs.withIdentity(identity).manualJobDeniedInCode({ id, denyIt: true }); // This would be false if a transaction rolled back. expect(await jobRan(id)).toBeTruthy(); @@ -233,12 +188,7 @@ test("job - exception - internal error without rollback transaction", async () = emailVerified: true, }); - await expect( - jobs.withIdentity(identity).manualJobWithException({ id }) - ).toHaveError({ - code: "ERR_UNKNOWN", - message: "something bad has happened!", - }); + await jobs.withIdentity(identity).manualJobWithException({ id }); // This would be false if a transaction rolled back. expect(await jobRan(id)).toBeTruthy(); @@ -249,10 +199,8 @@ test("scheduled job - without identity - permitted", async () => { id: "12345", didJobRun: false, }); - await expect( - jobs.scheduledWithoutPermissions({ scheduled: true }) - ).not.toHaveAuthorizationError(); + await jobs.scheduledWithoutPermissions({ scheduled: true }); expect(await jobRan(id)).toBeTruthy(); }); @@ -264,10 +212,9 @@ test("scheduled job - with identity - permitted", async () => { didJobRun: false, }); - await expect( - jobs.withIdentity(identity).scheduledWithoutPermissions({ scheduled: true }) - ).not.toHaveAuthorizationError(); - + await jobs + .withIdentity(identity) + .scheduledWithoutPermissions({ scheduled: true }); expect(await jobRan(id)).toBeTruthy(); }); @@ -277,11 +224,8 @@ test("scheduled job - invalid token - authentication failed", async () => { didJobRun: false, }); - await expect( - jobs - .withAuthToken("invalid") - .scheduledWithoutPermissions({ scheduled: true }) - ).toHaveAuthenticationError(); - + await jobs + .withAuthToken("invalid") + .scheduledWithoutPermissions({ scheduled: true }); expect(await jobRan(id)).toBeFalsy(); }); diff --git a/integration/testdata/subscribers_basic/subscribers/subscriberEnvvars.ts b/integration/testdata/subscribers_basic/subscribers/subscriberEnvvars.ts index 3cb07461f..cbe1249e9 100755 --- a/integration/testdata/subscribers_basic/subscribers/subscriberEnvvars.ts +++ b/integration/testdata/subscribers_basic/subscribers/subscriberEnvvars.ts @@ -7,6 +7,10 @@ export default SubscriberEnvvars(async (ctx, _) => { } const tracker = (await models.trackSubscriber.findMany())[0]; + if (!tracker) { + return; + } + await models.trackSubscriber.update( { id: tracker.id }, { didSubscriberRun: true } diff --git a/integration/testdata/subscribers_basic/subscribers/subscriberWithException.ts b/integration/testdata/subscribers_basic/subscribers/subscriberWithException.ts index 346466683..a9cc65d84 100755 --- a/integration/testdata/subscribers_basic/subscribers/subscriberWithException.ts +++ b/integration/testdata/subscribers_basic/subscribers/subscriberWithException.ts @@ -3,6 +3,10 @@ import { SubscriberWithException, models } from "@teamkeel/sdk"; // To learn more about events and subscribers, visit https://docs.keel.so/events export default SubscriberWithException(async (ctx, event) => { const tracker = (await models.trackSubscriber.findMany())[0]; + if (!tracker) { + return; + } + await models.trackSubscriber.update( { id: tracker.id }, { didSubscriberRun: true } diff --git a/node/bootstrap.go b/node/bootstrap.go index a418d3976..8473045b6 100644 --- a/node/bootstrap.go +++ b/node/bootstrap.go @@ -24,6 +24,15 @@ func GetDependencies(options *bootstrapOptions) (map[string]string, map[string]s functionsRuntimeVersion := runtime.GetVersion() testingRuntimeVersion := runtime.GetVersion() + // If doing a snapshot build with goreleaser the function will be something like -SNAPSHOT- which + // won't work, so we remove the "-SNAPSHOt-" bit + if strings.Contains(functionsRuntimeVersion, "-SNAPSHOT-") { + functionsRuntimeVersion = strings.Split(functionsRuntimeVersion, "-SNAPSHOT-")[0] + } + if strings.Contains(testingRuntimeVersion, "-SNAPSHOT-") { + testingRuntimeVersion = strings.Split(testingRuntimeVersion, "-SNAPSHOT-")[0] + } + // It is possible to reference a local version of our NPM modules rather than a version // from the NPM registry, by utilizing the --node-packages-path on the CLI. This flag is only applicable to the 'run' cmd at the moment, not 'generate'. if options.packagesPath != "" { diff --git a/packages/functions-runtime/src/File.js b/packages/functions-runtime/src/File.js index 51f971494..f5a110ad4 100644 --- a/packages/functions-runtime/src/File.js +++ b/packages/functions-runtime/src/File.js @@ -9,6 +9,35 @@ const { useDatabase } = require("./database"); const { DatabaseError } = require("./errors"); const KSUID = require("ksuid"); +const s3Client = (() => { + if (!process.env.KEEL_FILES_BUCKET_NAME) { + return null; + } + + // Set in integration tests to send all AWS API calls to a test server + // for mocking + const endpoint = process.env.TEST_AWS_ENDPOINT; + + return new S3Client({ + // If a test endpoint is provided then use some test credentials rather than fromEnv() + credentials: endpoint + ? { + accessKeyId: "test", + secretAccessKey: "test", + } + : fromEnv(), + + // If a custom endpoint is set we need to use a custom resolver. Just settng the base endpoint isn't enough for S3 as it + // as the default resolver uses the bucket name as a sub-domain, which likely won't work with the custom endpoint. + // By impleenting a full resolver we can force it to be the endpoint we want. + endpointProvider: () => { + return { + url: URL.parse(endpoint), + }; + }, + }); +})(); + class InlineFile { constructor({ filename, contentType }) { this._filename = filename; @@ -109,12 +138,7 @@ class File extends InlineFile { return Buffer.from(arrayBuffer); } - if (isS3Storage()) { - const s3Client = new S3Client({ - credentials: fromEnv(), - region: process.env.KEEL_REGION, - }); - + if (s3Client) { const params = { Bucket: process.env.KEEL_FILES_BUCKET_NAME, Key: "files/" + this.key, @@ -157,12 +181,7 @@ class File extends InlineFile { } async getPresignedUrl() { - if (isS3Storage()) { - const s3Client = new S3Client({ - credentials: fromEnv(), - region: process.env.KEEL_REGION, - }); - + if (s3Client) { const command = new GetObjectCommand({ Bucket: process.env.KEEL_FILES_BUCKET_NAME, Key: "files/" + this.key, @@ -203,22 +222,17 @@ class File extends InlineFile { } async function storeFile(contents, key, filename, contentType, expires) { - if (isS3Storage()) { - const s3Client = new S3Client({ - credentials: fromEnv(), - region: process.env.KEEL_REGION, - }); - + if (s3Client) { const params = { Bucket: process.env.KEEL_FILES_BUCKET_NAME, Key: "files/" + key, Body: contents, ContentType: contentType, ContentDisposition: `attachment; filename="${encodeURIComponent( - this.filename + filename )}"`, Metadata: { - filename: this.filename, + filename: filename, }, ACL: "private", }; @@ -269,10 +283,6 @@ async function storeFile(contents, key, filename, contentType, expires) { } } -function isS3Storage() { - return "KEEL_FILES_BUCKET_NAME" in process.env; -} - module.exports = { InlineFile, File, diff --git a/packages/testing-runtime/package.json b/packages/testing-runtime/package.json index 01996043d..c81c89e99 100644 --- a/packages/testing-runtime/package.json +++ b/packages/testing-runtime/package.json @@ -18,7 +18,7 @@ "prettier": "3.1.1" }, "dependencies": { - "@teamkeel/functions-runtime": "0.394.0", + "@teamkeel/functions-runtime": "file:../functions-runtime", "jsonwebtoken": "^9.0.2", "kysely": "^0.26.3", "lodash.ismatch": "^4.4.0", diff --git a/packages/testing-runtime/pnpm-lock.yaml b/packages/testing-runtime/pnpm-lock.yaml index 548d614e6..b9d531f37 100644 --- a/packages/testing-runtime/pnpm-lock.yaml +++ b/packages/testing-runtime/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@teamkeel/functions-runtime': - specifier: 0.394.0 - version: 0.394.0(@aws-sdk/client-sso-oidc@3.637.0) + specifier: file:../functions-runtime + version: file:../functions-runtime(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)) jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -82,6 +82,10 @@ packages: resolution: {integrity: sha512-i1x/E/sgA+liUE1XJ7rj1dhyXpAKO1UKFUcTTHXok2ARjWTvszHnSXMOsB77aPbmn0fUp1JTx2kHUAZ1LVt5Bg==} engines: {node: '>=16.0.0'} + '@aws-sdk/core@3.696.0': + resolution: {integrity: sha512-3c9III1k03DgvRZWg8vhVmfIXPG6hAciN9MzQTzqGngzWAELZF/WONRTRQuDFixVtarQatmLHYVw/atGeA2Byw==} + engines: {node: '>=16.0.0'} + '@aws-sdk/credential-provider-cognito-identity@3.637.0': resolution: {integrity: sha512-9qK1mF+EThtv3tsL1C/wb9MpWctJSkzjrLTFj+0Rtk8VYm6DlGepo/I6a2x3SeDmdBfHAFSrKFU39GqWDp1mwQ==} engines: {node: '>=16.0.0'} @@ -154,6 +158,10 @@ packages: resolution: {integrity: sha512-RLdYJPEV4JL/7NBoFUs7VlP90X++5FlJdxHz0DzCjmiD3qCviKy+Cym3qg1gBgHwucs5XisuClxDrGokhAdTQw==} engines: {node: '>=16.0.0'} + '@aws-sdk/middleware-sdk-s3@3.696.0': + resolution: {integrity: sha512-M7fEiAiN7DBMHflzOFzh1I2MNSlLpbiH2ubs87bdRc2wZsDPSbs4l3v6h3WLhxoQK0bq6vcfroudrLBgvCuX3Q==} + engines: {node: '>=16.0.0'} + '@aws-sdk/middleware-ssec@3.609.0': resolution: {integrity: sha512-GZSD1s7+JswWOTamVap79QiDaIV7byJFssBW68GYjyRS5EBjNfwA/8s+6uE6g39R3ojyTbYOmvcANoZEhSULXg==} engines: {node: '>=16.0.0'} @@ -166,10 +174,18 @@ packages: resolution: {integrity: sha512-vDCeMXvic/LU0KFIUjpC3RiSTIkkvESsEfbVHiHH0YINfl8HnEqR5rj+L8+phsCeVg2+LmYwYxd5NRz4PHxt5g==} engines: {node: '>=16.0.0'} + '@aws-sdk/s3-request-presigner@3.701.0': + resolution: {integrity: sha512-S4eKSZxhDcVmUoHv9N4dCxGde7V4v60R/+qFz/LgHxU++XOZ2npM/jqX5I9vT4uOkHLwQD6DgkL0j37vZpsqxA==} + engines: {node: '>=16.0.0'} + '@aws-sdk/signature-v4-multi-region@3.635.0': resolution: {integrity: sha512-J6QY4/invOkpogCHjSaDON1hF03viPpOnsrzVuCvJMmclS/iG62R4EY0wq1alYll0YmSdmKlpJwHMWwGtqK63Q==} engines: {node: '>=16.0.0'} + '@aws-sdk/signature-v4-multi-region@3.696.0': + resolution: {integrity: sha512-ijPkoLjXuPtgxAYlDoYls8UaG/VKigROn9ebbvPL/orEY5umedd3iZTcS9T+uAf4Ur3GELLxMQiERZpfDKaz3g==} + engines: {node: '>=16.0.0'} + '@aws-sdk/token-providers@3.614.0': resolution: {integrity: sha512-okItqyY6L9IHdxqs+Z116y5/nda7rHxLvROxtAJdLavWTYDydxrZstImNgGWTeVdmc0xX2gJCI77UYUTQWnhRw==} engines: {node: '>=16.0.0'} @@ -180,14 +196,26 @@ packages: resolution: {integrity: sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q==} engines: {node: '>=16.0.0'} + '@aws-sdk/types@3.696.0': + resolution: {integrity: sha512-9rTvUJIAj5d3//U5FDPWGJ1nFJLuWb30vugGOrWk7aNZ6y9tuA3PI7Cc9dP8WEXKVyK1vuuk8rSFP2iqXnlgrw==} + engines: {node: '>=16.0.0'} + '@aws-sdk/util-arn-parser@3.568.0': resolution: {integrity: sha512-XUKJWWo+KOB7fbnPP0+g/o5Ulku/X53t7i/h+sPHr5xxYTJJ9CYnbToo95mzxe7xWvkLrsNtJ8L+MnNn9INs2w==} engines: {node: '>=16.0.0'} + '@aws-sdk/util-arn-parser@3.693.0': + resolution: {integrity: sha512-WC8x6ca+NRrtpAH64rWu+ryDZI3HuLwlEr8EU6/dbC/pt+r/zC0PBoC15VEygUaBA+isppCikQpGyEDu0Yj7gQ==} + engines: {node: '>=16.0.0'} + '@aws-sdk/util-endpoints@3.637.0': resolution: {integrity: sha512-pAqOKUHeVWHEXXDIp/qoMk/6jyxIb6GGjnK1/f8dKHtKIEs4tKsnnL563gceEvdad53OPXIt86uoevCcCzmBnw==} engines: {node: '>=16.0.0'} + '@aws-sdk/util-format-url@3.696.0': + resolution: {integrity: sha512-R6yK1LozUD1GdAZRPhNsIow6VNFJUTyyoIar1OCWaknlucBMcq7musF3DN3TlORBwfFMj5buHc2ET9OtMtzvuA==} + engines: {node: '>=16.0.0'} + '@aws-sdk/util-locate-window@3.568.0': resolution: {integrity: sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig==} engines: {node: '>=16.0.0'} @@ -586,6 +614,10 @@ packages: resolution: {integrity: sha512-MBJBiidoe+0cTFhyxT8g+9g7CeVccLM0IOKKUMCNQ1CNMJ/eIfoo0RTfVrXOONEI1UCN1W+zkiHSbzUNE9dZtQ==} engines: {node: '>=16.0.0'} + '@smithy/abort-controller@3.1.8': + resolution: {integrity: sha512-+3DOBcUn5/rVjlxGvUPKc416SExarAQ+Qe0bqk30YSUjbepwpS7QN0cyKUSifvLJhdMZ0WPzPP5ymut0oonrpQ==} + engines: {node: '>=16.0.0'} + '@smithy/chunked-blob-reader-native@3.0.0': resolution: {integrity: sha512-VDkpCYW+peSuM4zJip5WDfqvg2Mo/e8yxOv3VF1m11y7B8KKMKVFtmZWDe36Fvk8rGuWrPZHHXZ7rR7uM5yWyg==} @@ -600,6 +632,10 @@ packages: resolution: {integrity: sha512-cHXq+FneIF/KJbt4q4pjN186+Jf4ZB0ZOqEaZMBhT79srEyGDDBV31NqBRBjazz8ppQ1bJbDJMY9ba5wKFV36w==} engines: {node: '>=16.0.0'} + '@smithy/core@2.5.4': + resolution: {integrity: sha512-iFh2Ymn2sCziBRLPuOOxRPkuCx/2gBdXtBGuCUFLUe6bWYjKnhHyIPqGeNkLZ5Aco/5GjebRTBFiWID3sDbrKw==} + engines: {node: '>=16.0.0'} + '@smithy/credential-provider-imds@3.2.0': resolution: {integrity: sha512-0SCIzgd8LYZ9EJxUjLXBmEKSZR/P/w6l7Rz/pab9culE/RWuqelAKGJvn5qUOl8BgX8Yj5HWM50A5hiB/RzsgA==} engines: {node: '>=16.0.0'} @@ -626,6 +662,9 @@ packages: '@smithy/fetch-http-handler@3.2.4': resolution: {integrity: sha512-kBprh5Gs5h7ug4nBWZi1FZthdqSM+T7zMmsZxx0IBvWUn7dK3diz2SHn7Bs4dQGFDk8plDv375gzenDoNwrXjg==} + '@smithy/fetch-http-handler@4.1.1': + resolution: {integrity: sha512-bH7QW0+JdX0bPBadXt8GwMof/jz0H28I84hU1Uet9ISpzUqXqRQ3fEZJ+ANPOhzSEczYvANNl3uDQDYArSFDtA==} + '@smithy/hash-blob-browser@3.1.2': resolution: {integrity: sha512-hAbfqN2UbISltakCC2TP0kx4LqXBttEv2MqSPE98gVuDFMf05lU+TpC41QtqGP3Ff5A3GwZMPfKnEy0VmEUpmg==} @@ -659,18 +698,34 @@ packages: resolution: {integrity: sha512-5y5aiKCEwg9TDPB4yFE7H6tYvGFf1OJHNczeY10/EFF8Ir8jZbNntQJxMWNfeQjC1mxPsaQ6mR9cvQbf+0YeMw==} engines: {node: '>=16.0.0'} + '@smithy/middleware-endpoint@3.2.4': + resolution: {integrity: sha512-TybiW2LA3kYVd3e+lWhINVu1o26KJbBwOpADnf0L4x/35vLVica77XVR5hvV9+kWeTGeSJ3IHTcYxbRxlbwhsg==} + engines: {node: '>=16.0.0'} + '@smithy/middleware-retry@3.0.15': resolution: {integrity: sha512-iTMedvNt1ApdvkaoE8aSDuwaoc+BhvHqttbA/FO4Ty+y/S5hW6Ci/CTScG7vam4RYJWZxdTElc3MEfHRVH6cgQ==} engines: {node: '>=16.0.0'} + '@smithy/middleware-serde@3.0.10': + resolution: {integrity: sha512-MnAuhh+dD14F428ubSJuRnmRsfOpxSzvRhaGVTvd/lrUDE3kxzCCmH8lnVTvoNQnV2BbJ4c15QwZ3UdQBtFNZA==} + engines: {node: '>=16.0.0'} + '@smithy/middleware-serde@3.0.3': resolution: {integrity: sha512-puUbyJQBcg9eSErFXjKNiGILJGtiqmuuNKEYNYfUD57fUl4i9+mfmThtQhvFXU0hCVG0iEJhvQUipUf+/SsFdA==} engines: {node: '>=16.0.0'} + '@smithy/middleware-stack@3.0.10': + resolution: {integrity: sha512-grCHyoiARDBBGPyw2BeicpjgpsDFWZZxptbVKb3CRd/ZA15F/T6rZjCCuBUjJwdck1nwUuIxYtsS4H9DDpbP5w==} + engines: {node: '>=16.0.0'} + '@smithy/middleware-stack@3.0.3': resolution: {integrity: sha512-r4klY9nFudB0r9UdSMaGSyjyQK5adUyPnQN/ZM6M75phTxOdnc/AhpvGD1fQUvgmqjQEBGCwpnPbDm8pH5PapA==} engines: {node: '>=16.0.0'} + '@smithy/node-config-provider@3.1.11': + resolution: {integrity: sha512-URq3gT3RpDikh/8MBJUB+QGZzfS7Bm6TQTqoh4CqE8NBuyPkWa5eUXj0XFcFfeZVgg3WMh1u19iaXn8FvvXxZw==} + engines: {node: '>=16.0.0'} + '@smithy/node-config-provider@3.1.4': resolution: {integrity: sha512-YvnElQy8HR4vDcAjoy7Xkx9YT8xZP4cBXcbJSgm/kxmiQu08DwUwj8rkGnyoJTpfl/3xYHH+d8zE+eHqoDCSdQ==} engines: {node: '>=16.0.0'} @@ -679,6 +734,14 @@ packages: resolution: {integrity: sha512-+UmxgixgOr/yLsUxcEKGH0fMNVteJFGkmRltYFHnBMlogyFdpzn2CwqWmxOrfJELhV34v0WSlaqG1UtE1uXlJg==} engines: {node: '>=16.0.0'} + '@smithy/node-http-handler@3.3.1': + resolution: {integrity: sha512-fr+UAOMGWh6bn4YSEezBCpJn9Ukp9oR4D32sCjCo7U81evE11YePOQ58ogzyfgmjIO79YeOdfXXqr0jyhPQeMg==} + engines: {node: '>=16.0.0'} + + '@smithy/property-provider@3.1.10': + resolution: {integrity: sha512-n1MJZGTorTH2DvyTVj+3wXnd4CzjJxyXeOgnTlgNVFxaaMeT4OteEp4QrzF8p9ee2yg42nvyVK6R/awLCakjeQ==} + engines: {node: '>=16.0.0'} + '@smithy/property-provider@3.1.3': resolution: {integrity: sha512-zahyOVR9Q4PEoguJ/NrFP4O7SMAfYO1HLhB18M+q+Z4KFd4V2obiMnlVoUFzFLSPeVt1POyNWneHHrZaTMoc/g==} engines: {node: '>=16.0.0'} @@ -687,10 +750,22 @@ packages: resolution: {integrity: sha512-dPVoHYQ2wcHooGXg3LQisa1hH0e4y0pAddPMeeUPipI1tEOqL6A4N0/G7abeq+K8wrwSgjk4C0wnD1XZpJm5aA==} engines: {node: '>=16.0.0'} + '@smithy/protocol-http@4.1.7': + resolution: {integrity: sha512-FP2LepWD0eJeOTm0SjssPcgqAlDFzOmRXqXmGhfIM52G7Lrox/pcpQf6RP4F21k0+O12zaqQt5fCDOeBtqY6Cg==} + engines: {node: '>=16.0.0'} + + '@smithy/querystring-builder@3.0.10': + resolution: {integrity: sha512-nT9CQF3EIJtIUepXQuBFb8dxJi3WVZS3XfuDksxSCSn+/CzZowRLdhDn+2acbBv8R6eaJqPupoI/aRFIImNVPQ==} + engines: {node: '>=16.0.0'} + '@smithy/querystring-builder@3.0.3': resolution: {integrity: sha512-vyWckeUeesFKzCDaRwWLUA1Xym9McaA6XpFfAK5qI9DKJ4M33ooQGqvM4J+LalH4u/Dq9nFiC8U6Qn1qi0+9zw==} engines: {node: '>=16.0.0'} + '@smithy/querystring-parser@3.0.10': + resolution: {integrity: sha512-Oa0XDcpo9SmjhiDD9ua2UyM3uU01ZTuIrNdZvzwUTykW1PM8o2yJvMh1Do1rY5sUQg4NDV70dMi0JhDx4GyxuQ==} + engines: {node: '>=16.0.0'} + '@smithy/querystring-parser@3.0.3': resolution: {integrity: sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ==} engines: {node: '>=16.0.0'} @@ -699,6 +774,10 @@ packages: resolution: {integrity: sha512-Jn39sSl8cim/VlkLsUhRFq/dKDnRUFlfRkvhOJaUbLBXUsLRLNf9WaxDv/z9BjuQ3A6k/qE8af1lsqcwm7+DaQ==} engines: {node: '>=16.0.0'} + '@smithy/shared-ini-file-loader@3.1.11': + resolution: {integrity: sha512-AUdrIZHFtUgmfSN4Gq9nHu3IkHMa1YDcN+s061Nfm+6pQ0mJy85YQDB0tZBCmls0Vuj22pLwDPmL92+Hvfwwlg==} + engines: {node: '>=16.0.0'} + '@smithy/shared-ini-file-loader@3.1.4': resolution: {integrity: sha512-qMxS4hBGB8FY2GQqshcRUy1K6k8aBWP5vwm8qKkCT3A9K2dawUwOIJfqh9Yste/Bl0J2lzosVyrXDj68kLcHXQ==} engines: {node: '>=16.0.0'} @@ -707,14 +786,29 @@ packages: resolution: {integrity: sha512-aRryp2XNZeRcOtuJoxjydO6QTaVhxx/vjaR+gx7ZjaFgrgPRyZ3HCTbfwqYj6ZWEBHkCSUfcaymKPURaByukag==} engines: {node: '>=16.0.0'} + '@smithy/signature-v4@4.2.3': + resolution: {integrity: sha512-pPSQQ2v2vu9vc8iew7sszLd0O09I5TRc5zhY71KA+Ao0xYazIG+uLeHbTJfIWGO3BGVLiXjUr3EEeCcEQLjpWQ==} + engines: {node: '>=16.0.0'} + '@smithy/smithy-client@3.2.0': resolution: {integrity: sha512-pDbtxs8WOhJLJSeaF/eAbPgXg4VVYFlRcL/zoNYA5WbG3wBL06CHtBSg53ppkttDpAJ/hdiede+xApip1CwSLw==} engines: {node: '>=16.0.0'} + '@smithy/smithy-client@3.4.5': + resolution: {integrity: sha512-k0sybYT9zlP79sIKd1XGm4TmK0AS1nA2bzDHXx7m0nGi3RQ8dxxQUs4CPkSmQTKAo+KF9aINU3KzpGIpV7UoMw==} + engines: {node: '>=16.0.0'} + '@smithy/types@3.3.0': resolution: {integrity: sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==} engines: {node: '>=16.0.0'} + '@smithy/types@3.7.1': + resolution: {integrity: sha512-XKLcLXZY7sUQgvvWyeaL/qwNPp6V3dWcUjqrQKjSb+tzYiCy340R/c64LV5j+Tnb2GhmunEX0eou+L+m2hJNYA==} + engines: {node: '>=16.0.0'} + + '@smithy/url-parser@3.0.10': + resolution: {integrity: sha512-j90NUalTSBR2NaZTuruEgavSdh8MLirf58LoGSk4AtQfyIymogIhgnGUU2Mga2bkMkpSoC9gxb74xBXL5afKAQ==} + '@smithy/url-parser@3.0.3': resolution: {integrity: sha512-pw3VtZtX2rg+s6HMs6/+u9+hu6oY6U7IohGhVNnjbgKy86wcIsSZwgHrFR+t67Uyxvp4Xz3p3kGXXIpTNisq8A==} @@ -757,6 +851,10 @@ packages: resolution: {integrity: sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==} engines: {node: '>=16.0.0'} + '@smithy/util-middleware@3.0.10': + resolution: {integrity: sha512-eJO+/+RsrG2RpmY68jZdwQtnfsxjmPxzMlQpnHKjFPwrYqvlcT+fHdT+ZVwcjlWSrByOhGr9Ff2GG17efc192A==} + engines: {node: '>=16.0.0'} + '@smithy/util-middleware@3.0.3': resolution: {integrity: sha512-l+StyYYK/eO3DlVPbU+4Bi06Jjal+PFLSMmlWM1BEwyLxZ3aKkf1ROnoIakfaA7mC6uw3ny7JBkau4Yc+5zfWw==} engines: {node: '>=16.0.0'} @@ -769,6 +867,10 @@ packages: resolution: {integrity: sha512-FIv/bRhIlAxC0U7xM1BCnF2aDRPq0UaelqBHkM2lsCp26mcBbgI0tCVTv+jGdsQLUmAMybua/bjDsSu8RQHbmw==} engines: {node: '>=16.0.0'} + '@smithy/util-stream@3.3.1': + resolution: {integrity: sha512-Ff68R5lJh2zj+AUTvbAU/4yx+6QPRzg7+pI7M1FbtQHcRIp7xvguxVsQBKyB3fwiOwhAKu0lnNyYBaQfSW6TNw==} + engines: {node: '>=16.0.0'} + '@smithy/util-uri-escape@3.0.0': resolution: {integrity: sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==} engines: {node: '>=16.0.0'} @@ -785,8 +887,8 @@ packages: resolution: {integrity: sha512-4pP0EV3iTsexDx+8PPGAKCQpd/6hsQBaQhqWzU4hqKPHN5epPsxKbvUTIiYIHTxaKt6/kEaqPBpu/ufvfbrRzw==} engines: {node: '>=16.0.0'} - '@teamkeel/functions-runtime@0.394.0': - resolution: {integrity: sha512-5Demy7yfctdTWXT6MR+j1Ze4l2+blh/UiU4EVsNfH8KBplHysVxXqFBVJEMaCQ2qFgs3Iyy8COvgRd2l6Pvl4w==} + '@teamkeel/functions-runtime@file:../functions-runtime': + resolution: {directory: ../functions-runtime, type: directory} '@types/chai-subset@1.3.5': resolution: {integrity: sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==} @@ -1348,7 +1450,7 @@ snapshots: '@aws-sdk/client-sso-oidc': 3.637.0(@aws-sdk/client-sts@3.637.0) '@aws-sdk/client-sts': 3.637.0 '@aws-sdk/core': 3.635.0 - '@aws-sdk/credential-provider-node': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0)(@aws-sdk/client-sts@3.637.0) + '@aws-sdk/credential-provider-node': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0))(@aws-sdk/client-sts@3.637.0) '@aws-sdk/middleware-host-header': 3.620.0 '@aws-sdk/middleware-logger': 3.609.0 '@aws-sdk/middleware-recursion-detection': 3.620.0 @@ -1395,7 +1497,7 @@ snapshots: '@aws-sdk/client-sso-oidc': 3.637.0(@aws-sdk/client-sts@3.637.0) '@aws-sdk/client-sts': 3.637.0 '@aws-sdk/core': 3.635.0 - '@aws-sdk/credential-provider-node': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0)(@aws-sdk/client-sts@3.637.0) + '@aws-sdk/credential-provider-node': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0))(@aws-sdk/client-sts@3.637.0) '@aws-sdk/middleware-bucket-endpoint': 3.620.0 '@aws-sdk/middleware-expect-continue': 3.620.0 '@aws-sdk/middleware-flexible-checksums': 3.620.0 @@ -1456,7 +1558,7 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-sdk/client-sts': 3.637.0 '@aws-sdk/core': 3.635.0 - '@aws-sdk/credential-provider-node': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0)(@aws-sdk/client-sts@3.637.0) + '@aws-sdk/credential-provider-node': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0))(@aws-sdk/client-sts@3.637.0) '@aws-sdk/middleware-host-header': 3.620.0 '@aws-sdk/middleware-logger': 3.609.0 '@aws-sdk/middleware-recursion-detection': 3.620.0 @@ -1544,7 +1646,7 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-sdk/client-sso-oidc': 3.637.0(@aws-sdk/client-sts@3.637.0) '@aws-sdk/core': 3.635.0 - '@aws-sdk/credential-provider-node': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0)(@aws-sdk/client-sts@3.637.0) + '@aws-sdk/credential-provider-node': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0))(@aws-sdk/client-sts@3.637.0) '@aws-sdk/middleware-host-header': 3.620.0 '@aws-sdk/middleware-logger': 3.609.0 '@aws-sdk/middleware-recursion-detection': 3.620.0 @@ -1596,6 +1698,20 @@ snapshots: fast-xml-parser: 4.4.1 tslib: 2.7.0 + '@aws-sdk/core@3.696.0': + dependencies: + '@aws-sdk/types': 3.696.0 + '@smithy/core': 2.5.4 + '@smithy/node-config-provider': 3.1.11 + '@smithy/property-provider': 3.1.10 + '@smithy/protocol-http': 4.1.7 + '@smithy/signature-v4': 4.2.3 + '@smithy/smithy-client': 3.4.5 + '@smithy/types': 3.7.1 + '@smithy/util-middleware': 3.0.10 + fast-xml-parser: 4.4.1 + tslib: 2.7.0 + '@aws-sdk/credential-provider-cognito-identity@3.637.0': dependencies: '@aws-sdk/client-cognito-identity': 3.637.0 @@ -1625,13 +1741,13 @@ snapshots: '@smithy/util-stream': 3.1.3 tslib: 2.7.0 - '@aws-sdk/credential-provider-ini@3.637.0(@aws-sdk/client-sso-oidc@3.637.0)(@aws-sdk/client-sts@3.637.0)': + '@aws-sdk/credential-provider-ini@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0))(@aws-sdk/client-sts@3.637.0)': dependencies: '@aws-sdk/client-sts': 3.637.0 '@aws-sdk/credential-provider-env': 3.620.1 '@aws-sdk/credential-provider-http': 3.635.0 '@aws-sdk/credential-provider-process': 3.620.1 - '@aws-sdk/credential-provider-sso': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0) + '@aws-sdk/credential-provider-sso': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)) '@aws-sdk/credential-provider-web-identity': 3.621.0(@aws-sdk/client-sts@3.637.0) '@aws-sdk/types': 3.609.0 '@smithy/credential-provider-imds': 3.2.0 @@ -1643,13 +1759,13 @@ snapshots: - '@aws-sdk/client-sso-oidc' - aws-crt - '@aws-sdk/credential-provider-node@3.637.0(@aws-sdk/client-sso-oidc@3.637.0)(@aws-sdk/client-sts@3.637.0)': + '@aws-sdk/credential-provider-node@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0))(@aws-sdk/client-sts@3.637.0)': dependencies: '@aws-sdk/credential-provider-env': 3.620.1 '@aws-sdk/credential-provider-http': 3.635.0 - '@aws-sdk/credential-provider-ini': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0)(@aws-sdk/client-sts@3.637.0) + '@aws-sdk/credential-provider-ini': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0))(@aws-sdk/client-sts@3.637.0) '@aws-sdk/credential-provider-process': 3.620.1 - '@aws-sdk/credential-provider-sso': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0) + '@aws-sdk/credential-provider-sso': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)) '@aws-sdk/credential-provider-web-identity': 3.621.0(@aws-sdk/client-sts@3.637.0) '@aws-sdk/types': 3.609.0 '@smithy/credential-provider-imds': 3.2.0 @@ -1670,10 +1786,10 @@ snapshots: '@smithy/types': 3.3.0 tslib: 2.7.0 - '@aws-sdk/credential-provider-sso@3.637.0(@aws-sdk/client-sso-oidc@3.637.0)': + '@aws-sdk/credential-provider-sso@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0))': dependencies: '@aws-sdk/client-sso': 3.637.0 - '@aws-sdk/token-providers': 3.614.0(@aws-sdk/client-sso-oidc@3.637.0) + '@aws-sdk/token-providers': 3.614.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)) '@aws-sdk/types': 3.609.0 '@smithy/property-provider': 3.1.3 '@smithy/shared-ini-file-loader': 3.1.4 @@ -1691,7 +1807,7 @@ snapshots: '@smithy/types': 3.3.0 tslib: 2.7.0 - '@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0)': + '@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0))': dependencies: '@aws-sdk/client-cognito-identity': 3.637.0 '@aws-sdk/client-sso': 3.637.0 @@ -1699,10 +1815,10 @@ snapshots: '@aws-sdk/credential-provider-cognito-identity': 3.637.0 '@aws-sdk/credential-provider-env': 3.620.1 '@aws-sdk/credential-provider-http': 3.635.0 - '@aws-sdk/credential-provider-ini': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0)(@aws-sdk/client-sts@3.637.0) - '@aws-sdk/credential-provider-node': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0)(@aws-sdk/client-sts@3.637.0) + '@aws-sdk/credential-provider-ini': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0))(@aws-sdk/client-sts@3.637.0) + '@aws-sdk/credential-provider-node': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0))(@aws-sdk/client-sts@3.637.0) '@aws-sdk/credential-provider-process': 3.620.1 - '@aws-sdk/credential-provider-sso': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0) + '@aws-sdk/credential-provider-sso': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)) '@aws-sdk/credential-provider-web-identity': 3.621.0(@aws-sdk/client-sts@3.637.0) '@aws-sdk/types': 3.609.0 '@smithy/credential-provider-imds': 3.2.0 @@ -1784,6 +1900,23 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.7.0 + '@aws-sdk/middleware-sdk-s3@3.696.0': + dependencies: + '@aws-sdk/core': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@aws-sdk/util-arn-parser': 3.693.0 + '@smithy/core': 2.5.4 + '@smithy/node-config-provider': 3.1.11 + '@smithy/protocol-http': 4.1.7 + '@smithy/signature-v4': 4.2.3 + '@smithy/smithy-client': 3.4.5 + '@smithy/types': 3.7.1 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-stream': 3.3.1 + '@smithy/util-utf8': 3.0.0 + tslib: 2.7.0 + '@aws-sdk/middleware-ssec@3.609.0': dependencies: '@aws-sdk/types': 3.609.0 @@ -1807,6 +1940,17 @@ snapshots: '@smithy/util-middleware': 3.0.3 tslib: 2.7.0 + '@aws-sdk/s3-request-presigner@3.701.0': + dependencies: + '@aws-sdk/signature-v4-multi-region': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@aws-sdk/util-format-url': 3.696.0 + '@smithy/middleware-endpoint': 3.2.4 + '@smithy/protocol-http': 4.1.7 + '@smithy/smithy-client': 3.4.5 + '@smithy/types': 3.7.1 + tslib: 2.7.0 + '@aws-sdk/signature-v4-multi-region@3.635.0': dependencies: '@aws-sdk/middleware-sdk-s3': 3.635.0 @@ -1816,7 +1960,16 @@ snapshots: '@smithy/types': 3.3.0 tslib: 2.7.0 - '@aws-sdk/token-providers@3.614.0(@aws-sdk/client-sso-oidc@3.637.0)': + '@aws-sdk/signature-v4-multi-region@3.696.0': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@smithy/protocol-http': 4.1.7 + '@smithy/signature-v4': 4.2.3 + '@smithy/types': 3.7.1 + tslib: 2.7.0 + + '@aws-sdk/token-providers@3.614.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0))': dependencies: '@aws-sdk/client-sso-oidc': 3.637.0(@aws-sdk/client-sts@3.637.0) '@aws-sdk/types': 3.609.0 @@ -1830,10 +1983,19 @@ snapshots: '@smithy/types': 3.3.0 tslib: 2.7.0 + '@aws-sdk/types@3.696.0': + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.7.0 + '@aws-sdk/util-arn-parser@3.568.0': dependencies: tslib: 2.7.0 + '@aws-sdk/util-arn-parser@3.693.0': + dependencies: + tslib: 2.7.0 + '@aws-sdk/util-endpoints@3.637.0': dependencies: '@aws-sdk/types': 3.609.0 @@ -1841,6 +2003,13 @@ snapshots: '@smithy/util-endpoints': 2.0.5 tslib: 2.7.0 + '@aws-sdk/util-format-url@3.696.0': + dependencies: + '@aws-sdk/types': 3.696.0 + '@smithy/querystring-builder': 3.0.10 + '@smithy/types': 3.7.1 + tslib: 2.7.0 + '@aws-sdk/util-locate-window@3.568.0': dependencies: tslib: 2.7.0 @@ -2137,6 +2306,11 @@ snapshots: '@smithy/types': 3.3.0 tslib: 2.7.0 + '@smithy/abort-controller@3.1.8': + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.7.0 + '@smithy/chunked-blob-reader-native@3.0.0': dependencies: '@smithy/util-base64': 3.0.0 @@ -2167,6 +2341,17 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.7.0 + '@smithy/core@2.5.4': + dependencies: + '@smithy/middleware-serde': 3.0.10 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-stream': 3.3.1 + '@smithy/util-utf8': 3.0.0 + tslib: 2.7.0 + '@smithy/credential-provider-imds@3.2.0': dependencies: '@smithy/node-config-provider': 3.1.4 @@ -2213,6 +2398,14 @@ snapshots: '@smithy/util-base64': 3.0.0 tslib: 2.7.0 + '@smithy/fetch-http-handler@4.1.1': + dependencies: + '@smithy/protocol-http': 4.1.7 + '@smithy/querystring-builder': 3.0.10 + '@smithy/types': 3.7.1 + '@smithy/util-base64': 3.0.0 + tslib: 2.7.0 + '@smithy/hash-blob-browser@3.1.2': dependencies: '@smithy/chunked-blob-reader': 3.0.0 @@ -2268,6 +2461,17 @@ snapshots: '@smithy/util-middleware': 3.0.3 tslib: 2.7.0 + '@smithy/middleware-endpoint@3.2.4': + dependencies: + '@smithy/core': 2.5.4 + '@smithy/middleware-serde': 3.0.10 + '@smithy/node-config-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.11 + '@smithy/types': 3.7.1 + '@smithy/url-parser': 3.0.10 + '@smithy/util-middleware': 3.0.10 + tslib: 2.7.0 + '@smithy/middleware-retry@3.0.15': dependencies: '@smithy/node-config-provider': 3.1.4 @@ -2280,16 +2484,33 @@ snapshots: tslib: 2.7.0 uuid: 9.0.1 + '@smithy/middleware-serde@3.0.10': + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.7.0 + '@smithy/middleware-serde@3.0.3': dependencies: '@smithy/types': 3.3.0 tslib: 2.7.0 + '@smithy/middleware-stack@3.0.10': + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.7.0 + '@smithy/middleware-stack@3.0.3': dependencies: '@smithy/types': 3.3.0 tslib: 2.7.0 + '@smithy/node-config-provider@3.1.11': + dependencies: + '@smithy/property-provider': 3.1.10 + '@smithy/shared-ini-file-loader': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.7.0 + '@smithy/node-config-provider@3.1.4': dependencies: '@smithy/property-provider': 3.1.3 @@ -2305,6 +2526,19 @@ snapshots: '@smithy/types': 3.3.0 tslib: 2.7.0 + '@smithy/node-http-handler@3.3.1': + dependencies: + '@smithy/abort-controller': 3.1.8 + '@smithy/protocol-http': 4.1.7 + '@smithy/querystring-builder': 3.0.10 + '@smithy/types': 3.7.1 + tslib: 2.7.0 + + '@smithy/property-provider@3.1.10': + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.7.0 + '@smithy/property-provider@3.1.3': dependencies: '@smithy/types': 3.3.0 @@ -2315,12 +2549,28 @@ snapshots: '@smithy/types': 3.3.0 tslib: 2.7.0 + '@smithy/protocol-http@4.1.7': + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.7.0 + + '@smithy/querystring-builder@3.0.10': + dependencies: + '@smithy/types': 3.7.1 + '@smithy/util-uri-escape': 3.0.0 + tslib: 2.7.0 + '@smithy/querystring-builder@3.0.3': dependencies: '@smithy/types': 3.3.0 '@smithy/util-uri-escape': 3.0.0 tslib: 2.7.0 + '@smithy/querystring-parser@3.0.10': + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.7.0 + '@smithy/querystring-parser@3.0.3': dependencies: '@smithy/types': 3.3.0 @@ -2330,6 +2580,11 @@ snapshots: dependencies: '@smithy/types': 3.3.0 + '@smithy/shared-ini-file-loader@3.1.11': + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.7.0 + '@smithy/shared-ini-file-loader@3.1.4': dependencies: '@smithy/types': 3.3.0 @@ -2346,6 +2601,17 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.7.0 + '@smithy/signature-v4@4.2.3': + dependencies: + '@smithy/is-array-buffer': 3.0.0 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + '@smithy/util-hex-encoding': 3.0.0 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-uri-escape': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.7.0 + '@smithy/smithy-client@3.2.0': dependencies: '@smithy/middleware-endpoint': 3.1.0 @@ -2355,10 +2621,30 @@ snapshots: '@smithy/util-stream': 3.1.3 tslib: 2.7.0 + '@smithy/smithy-client@3.4.5': + dependencies: + '@smithy/core': 2.5.4 + '@smithy/middleware-endpoint': 3.2.4 + '@smithy/middleware-stack': 3.0.10 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + '@smithy/util-stream': 3.3.1 + tslib: 2.7.0 + '@smithy/types@3.3.0': dependencies: tslib: 2.7.0 + '@smithy/types@3.7.1': + dependencies: + tslib: 2.7.0 + + '@smithy/url-parser@3.0.10': + dependencies: + '@smithy/querystring-parser': 3.0.10 + '@smithy/types': 3.7.1 + tslib: 2.7.0 + '@smithy/url-parser@3.0.3': dependencies: '@smithy/querystring-parser': 3.0.3 @@ -2421,6 +2707,11 @@ snapshots: dependencies: tslib: 2.7.0 + '@smithy/util-middleware@3.0.10': + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.7.0 + '@smithy/util-middleware@3.0.3': dependencies: '@smithy/types': 3.3.0 @@ -2443,6 +2734,17 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.7.0 + '@smithy/util-stream@3.3.1': + dependencies: + '@smithy/fetch-http-handler': 4.1.1 + '@smithy/node-http-handler': 3.3.1 + '@smithy/types': 3.7.1 + '@smithy/util-base64': 3.0.0 + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-hex-encoding': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.7.0 + '@smithy/util-uri-escape@3.0.0': dependencies: tslib: 2.7.0 @@ -2463,10 +2765,11 @@ snapshots: '@smithy/types': 3.3.0 tslib: 2.7.0 - '@teamkeel/functions-runtime@0.394.0(@aws-sdk/client-sso-oidc@3.637.0)': + '@teamkeel/functions-runtime@file:../functions-runtime(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0))': dependencies: '@aws-sdk/client-s3': 3.637.0 - '@aws-sdk/credential-providers': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0) + '@aws-sdk/credential-providers': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)) + '@aws-sdk/s3-request-presigner': 3.701.0 '@neondatabase/serverless': 0.9.4 '@opentelemetry/api': 1.9.0 '@opentelemetry/exporter-trace-otlp-proto': 0.46.0(@opentelemetry/api@1.9.0) @@ -2980,11 +3283,11 @@ snapshots: vite@5.3.5(@types/node@22.0.0): dependencies: - '@types/node': 22.0.0 esbuild: 0.21.5 postcss: 8.4.40 rollup: 4.19.1 optionalDependencies: + '@types/node': 22.0.0 fsevents: 2.3.3 vitest@0.34.6: diff --git a/storage/storage.go b/storage/storage.go index a5cd3988f..d12f79053 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -6,6 +6,7 @@ import ( ) // Storer represents the interface for a file storing service that is used by the Keel runtime +// TODO: all these methods should take context as first arg type Storer interface { // Store will save the given file and return a FileInfo struct for it // diff --git a/testing/aws.go b/testing/aws.go new file mode 100644 index 000000000..a16dcfd83 --- /dev/null +++ b/testing/aws.go @@ -0,0 +1,178 @@ +package testing + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/sqs" + "github.com/aws/aws-sdk-go/service/ssm" +) + +type S3Object struct { + Headers http.Header + Data []byte +} + +type AWSAPIHandler struct { + PathPrefix string + SSMParameters map[string]string + FunctionsURL string + FunctionsARN string + S3Bucket map[string]*S3Object + OnSQSEvent func(events.SQSEvent) +} + +func (h *AWSAPIHandler) HandleHTTP(r *http.Request, w http.ResponseWriter) { + if h.S3Bucket == nil { + h.S3Bucket = map[string]*S3Object{} + } + + s3Prefixes := []string{ + h.PathPrefix + "files/", + h.PathPrefix + "jobs/", + } + isS3 := func() bool { + for _, prefix := range s3Prefixes { + if strings.HasPrefix(r.URL.Path, prefix) { + return true + } + } + return false + } + + switch { + case r.Header.Get("X-Amz-Target") == "AmazonSSM.GetParameters": + h.ssmGetParameters(w) + return + case r.Header.Get("X-Amz-Target") == "AmazonSQS.SendMessage": + h.sqsSendMessage(r, w) + return + case r.URL.Path == fmt.Sprintf("/aws/2015-03-31/functions/%s/invocations", h.FunctionsARN): + h.lambdaInvoke(r, w) + case r.Method == http.MethodPut && isS3(): + h.s3PutObject(r, w) + return + case r.Method == http.MethodGet && isS3(): + h.s3GetObject(r, w) + return + default: + fmt.Println("Unhandled AWS request", r.Method, r.URL.Path, r.Header) + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("")) + return + } +} + +// https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_GetParameters.html +func (h *AWSAPIHandler) ssmGetParameters(w http.ResponseWriter) { + res := ssm.GetParametersOutput{} + for k, v := range h.SSMParameters { + res.Parameters = append(res.Parameters, &ssm.Parameter{ + Name: aws.String(k), + Value: aws.String(v), + }) + } + writeJSON(w, http.StatusOK, res) +} + +// https://docs.aws.amazon.com/lambda/latest/api/API_Invoke.html +func (h *AWSAPIHandler) lambdaInvoke(r *http.Request, w http.ResponseWriter) { + requestBody, err := io.ReadAll(r.Body) + if err != nil { + writeJSON(w, http.StatusInternalServerError, nil) + return + } + + functionsResponse, err := http.Post(h.FunctionsURL, "application/json", bytes.NewReader(requestBody)) + if err != nil { + writeJSON(w, http.StatusInternalServerError, nil) + return + } + + responseBody, err := io.ReadAll(functionsResponse.Body) + if err != nil { + writeJSON(w, http.StatusInternalServerError, nil) + return + } + + w.WriteHeader(functionsResponse.StatusCode) + w.Header().Set("content-type", "application/json") + _, _ = w.Write(responseBody) +} + +// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html +func (h *AWSAPIHandler) s3PutObject(r *http.Request, w http.ResponseWriter) { + b, err := io.ReadAll(r.Body) + if err != nil { + writeJSON(w, http.StatusInternalServerError, nil) + return + } + + key := strings.TrimPrefix(r.URL.Path, h.PathPrefix) + + h.S3Bucket[key] = &S3Object{ + // Store the headers so we can return them in GetObject + Headers: r.Header.Clone(), + Data: b, + } + + _, _ = w.Write([]byte("")) +} + +// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html +func (h *AWSAPIHandler) s3GetObject(r *http.Request, w http.ResponseWriter) { + key := strings.TrimPrefix(r.URL.Path, h.PathPrefix) + + f, ok := h.S3Bucket[key] + if !ok { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("")) + return + } + + for key, values := range f.Headers { + // From what I can tell S3 just returns these headers as they were used in PutObject + if strings.HasPrefix(key, "Content-") || strings.HasPrefix(key, "X-Amz-") { + for _, v := range values { + w.Header().Add(key, v) + } + } + } + + _, _ = w.Write(f.Data) +} + +// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html +func (h *AWSAPIHandler) sqsSendMessage(r *http.Request, w http.ResponseWriter) { + requestBody, err := io.ReadAll(r.Body) + if err != nil { + writeJSON(w, http.StatusInternalServerError, nil) + return + } + + input := sqs.SendMessageInput{} + err = json.Unmarshal(requestBody, &input) + if err != nil { + writeJSON(w, http.StatusInternalServerError, nil) + return + } + + event := events.SQSEvent{ + Records: []events.SQSMessage{ + { + MessageId: "test-message", + Body: *input.MessageBody, + }, + }, + } + + h.OnSQSEvent(event) + + writeJSON(w, http.StatusOK, nil) +} diff --git a/testing/testing.go b/testing/testing.go index 9f29f3a60..89a091cec 100644 --- a/testing/testing.go +++ b/testing/testing.go @@ -1,6 +1,7 @@ package testing import ( + "bytes" "context" "crypto/x509" _ "embed" @@ -13,32 +14,32 @@ import ( "net/http" "os" "os/exec" + "path" "strings" + "time" + + lambdaevents "github.com/aws/aws-lambda-go/events" + "github.com/iancoleman/strcase" + "github.com/segmentio/ksuid" + "go.opentelemetry.io/otel" "github.com/teamkeel/keel/db" + "github.com/teamkeel/keel/deploy" + "github.com/teamkeel/keel/deploy/lambdas/runtime" "github.com/teamkeel/keel/events" - "github.com/teamkeel/keel/functions" "github.com/teamkeel/keel/node" "github.com/teamkeel/keel/proto" - "github.com/teamkeel/keel/runtime" - "github.com/teamkeel/keel/runtime/actions" "github.com/teamkeel/keel/runtime/apis/httpjson" - "github.com/teamkeel/keel/runtime/auth" - "github.com/teamkeel/keel/runtime/locale" - "github.com/teamkeel/keel/runtime/runtimectx" - "github.com/teamkeel/keel/schema" - "github.com/teamkeel/keel/storage" "github.com/teamkeel/keel/testhelpers" "github.com/teamkeel/keel/util" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" ) const ( - AuthPath = "auth" - ActionApiPath = "testingactionsapi" - JobPath = "testingjobs" - SubscriberPath = "testingsubscribers" + AuthPath = "auth" + ActionApiPath = "testingactionsapi" + JobPath = "testingjobs" + SubscriberPath = "testingsubscribers" + JobsWebhookPath = "/webhooks/jobs" ) type TestOutput struct { @@ -59,37 +60,44 @@ type RunnerOpts struct { var tracer = otel.Tracer("github.com/teamkeel/keel/testing") func Run(ctx context.Context, opts *RunnerOpts) error { - builder := &schema.Builder{} - - schema, err := builder.MakeFromDirectory(opts.Dir) - if err != nil { - return err - } + buildRessult, err := deploy.Build(ctx, &deploy.BuildArgs{ + ProjectRoot: opts.Dir, + Env: "test", + SkipRuntimeBinary: true, + OnLoadSchema: func(schema *proto.Schema) *proto.Schema { + testApi := &proto.Api{ + Name: ActionApiPath, + } - envVars := builder.Config.GetEnvVars() + for _, m := range schema.Models { + apiModel := &proto.ApiModel{ + ModelName: m.Name, + ModelActions: []*proto.ApiModelAction{}, + } - testApi := &proto.Api{ - // TODO: make random so doesn't clash - Name: ActionApiPath, - } - for _, m := range schema.Models { - apiModel := &proto.ApiModel{ - ModelName: m.Name, - ModelActions: []*proto.ApiModelAction{}, - } + testApi.ApiModels = append(testApi.ApiModels, apiModel) + for _, a := range m.Actions { + apiModel.ModelActions = append(apiModel.ModelActions, &proto.ApiModelAction{ActionName: a.Name}) + } + } - testApi.ApiModels = append(testApi.ApiModels, apiModel) - for _, a := range m.Actions { - apiModel.ModelActions = append(apiModel.ModelActions, &proto.ApiModelAction{ActionName: a.Name}) - } + schema.Apis = append(schema.Apis, testApi) + return schema + }, + }) + if err != nil { + return err } - schema.Apis = append(schema.Apis, testApi) + schema := buildRessult.Schema + config := buildRessult.Config + envVars := config.GetEnvVars() spanName := opts.TestGroupName if spanName == "" { spanName = "testing.Run" } + ctx, span := tracer.Start(ctx, spanName) defer span.End() @@ -102,16 +110,6 @@ func Run(ctx context.Context, opts *RunnerOpts) error { dbConnString := opts.DbConnInfo.WithDatabase(dbName).String() - files, err := node.Generate( - ctx, - schema, - builder.Config, - node.WithDevelopmentServer(true), - ) - if err != nil { - return err - } - if opts.GenerateClient { clientFiles, err := node.GenerateClient( ctx, @@ -123,21 +121,41 @@ func Run(ctx context.Context, opts *RunnerOpts) error { return err } - files = append(files, clientFiles...) + err = clientFiles.Write(opts.Dir) + if err != nil { + return err + } + } + + err = os.WriteFile(path.Join(opts.Dir, ".build/server.js"), []byte(functionsServerEntry), os.ModePerm) + if err != nil { + return err } - err = files.Write(opts.Dir) + runtimePort, err := util.GetFreePort() if err != nil { return err } + serverURL := fmt.Sprintf("http://localhost:%s", runtimePort) + bucketName := "testing-bucket-name" + functionsARN := "arn:test:lambda:functions:function" + var functionsServer *node.DevelopmentServer - var functionsTransport functions.Transport - if node.HasFunctions(schema, builder.Config) { + if node.HasFunctions(schema, config) { functionEnvVars := map[string]string{ - "KEEL_DB_CONN_TYPE": "pg", - "KEEL_TRACING_ENABLED": os.Getenv("TRACING_ENABLED"), + "KEEL_DB_CONN_TYPE": "pg", + "KEEL_TRACING_ENABLED": os.Getenv("TRACING_ENABLED"), + "KEEL_FILES_BUCKET_NAME": bucketName, + + // Send all AWS API calls to our test server and set some test credentials + "TEST_AWS_ENDPOINT": fmt.Sprintf("%s/aws", serverURL), + "AWS_ACCESS_KEY_ID": "test", + "AWS_SECRET_ACCESS_KEY": "test", + "AWS_SESSION_TOKEN": "test", + "AWS_REGION": "test", + "OTEL_RESOURCE_ATTRIBUTES": "service.name=functions", } @@ -149,6 +167,7 @@ func Run(ctx context.Context, opts *RunnerOpts) error { EnvVars: functionEnvVars, Output: os.Stdout, Debug: os.Getenv("DEBUG_FUNCTIONS") == "true", + Watch: false, }) if err != nil { @@ -161,101 +180,112 @@ func Run(ctx context.Context, opts *RunnerOpts) error { defer func() { _ = functionsServer.Kill() }() + } - functionsTransport = functions.NewHttpTransport(functionsServer.URL) + for key, value := range envVars { + os.Setenv(key, value) } - runtimePort, err := util.GetFreePort() + pk, err := testhelpers.GetEmbeddedPrivateKey() if err != nil { return err } - for key, value := range envVars { - os.Setenv(key, value) + pkBytes := x509.MarshalPKCS1PrivateKey(pk) + pkPem := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: pkBytes, + }, + ) + + pkBase64 := base64.StdEncoding.EncodeToString(pkPem) + + var lambdaHandler *runtime.Handler + + awsHandler := &AWSAPIHandler{ + PathPrefix: "/aws/", + FunctionsARN: functionsARN, + SSMParameters: map[string]string{ + "KEEL_PRIVATE_KEY": string(pkPem), + "DATABASE_URL": dbConnString, + }, + OnSQSEvent: func(event lambdaevents.SQSEvent) { + // TODO: consider doing this in a go routine to make it async + // but current tests require it to be sync + err := lambdaHandler.EventHandler(ctx, event) + if err != nil { + fmt.Printf("error from event handler: %s\nevent:%s\n\n", err.Error(), event.Records[0].Body) + } + }, + } + if functionsServer != nil { + awsHandler.FunctionsURL = functionsServer.URL } - // Server to handle receiving HTTP requests from the ActionExecutor, JobExecutor and SubscriberExecutor. + // This server handles requests from the ActionExecutor, JobExecutor and SubscriberExecutor in the Vitest tests + // but also AWS API calls which come here because we set a custom endpoint on the clients runtimeServer := http.Server{ Addr: fmt.Sprintf(":%s", runtimePort), Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx, span := tracer.Start(r.Context(), strings.Trim(r.URL.Path, "/")) defer span.End() - storer, err := storage.NewDbStore(context.Background(), database) - if err != nil { - panic(err) + // Handle AWS API call + if strings.HasPrefix(r.URL.Path, "/aws") { + awsHandler.HandleHTTP(r, w) + return } - ctx = runtimectx.WithEnv(ctx, runtimectx.KeelEnvTest) - ctx = db.WithDatabase(ctx, database) - - // Pass db conn as secret - we do this here as we change the db name in this function - // so can't set it in the secrets passed in as options - opts.Secrets["KEEL_DB_CONN"] = dbConnString - ctx = runtimectx.WithSecrets(ctx, opts.Secrets) - - ctx = runtimectx.WithOAuthConfig(ctx, &builder.Config.Auth) - ctx = runtimectx.WithStorage(ctx, storer) - - span.SetAttributes(attribute.String("request.url", r.URL.String())) - - // Use the embedded private key for the tests - pk, err := testhelpers.GetEmbeddedPrivateKey() - if err != nil { - panic(err) - } - - if pk == nil { - panic("No private key") - } - - ctx = runtimectx.WithPrivateKey(ctx, pk) - - if functionsTransport != nil { - ctx = functions.WithFunctionsTransport(ctx, functionsTransport) - } - - // Synchronous event handling - ctx, err = events.WithEventHandler(ctx, func(ctx context.Context, subscriber string, event *events.Event, traceparent string) error { - return runtime.NewSubscriberHandler(schema).RunSubscriber(ctx, subscriber, event) - }) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(err.Error())) + if r.URL.Path == JobsWebhookPath { + // not doing anything with this for now but can do in the future... + writeJSON(w, http.StatusOK, map[string]any{}) + return } + // Handle API calls, jobs and subscriber executors pathParts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") switch pathParts[0] { - case AuthPath: - r = r.WithContext(ctx) - runtime.NewHttpHandler(schema).ServeHTTP(w, r) - case ActionApiPath: - r = r.WithContext(ctx) - runtime.NewHttpHandler(schema).ServeHTTP(w, r) - case JobPath: - if len(pathParts) != 3 { - w.WriteHeader(http.StatusBadRequest) + case ActionApiPath, AuthPath: + e, err := toLambdaFunctionURLRequest(r) + if err != nil { + writeJSON(w, http.StatusInternalServerError, err.Error()) return } - err := HandleJobExecutorRequest(ctx, schema, pathParts[2], r) + res, err := lambdaHandler.APIHandler(ctx, e) + if err != nil { + writeJSON(w, http.StatusInternalServerError, err.Error()) + return + } + for k, v := range res.Headers { + w.Header().Set(k, v) + } + w.WriteHeader(res.StatusCode) + _, _ = w.Write([]byte(res.Body)) + case JobPath: + err := HandleJobExecutorRequest(r, lambdaHandler, awsHandler) if err != nil { response := httpjson.NewErrorResponse(ctx, err, nil) w.WriteHeader(response.Status) _, _ = w.Write(response.Body) - } - case SubscriberPath: - if len(pathParts) != 3 { - w.WriteHeader(http.StatusBadRequest) return } - err := HandleSubscriberExecutorRequest(ctx, schema, pathParts[2], r) + + writeJSON(w, http.StatusOK, map[string]any{}) + return + case SubscriberPath: + err = HandleSubscriberExecutorRequest(r.WithContext(ctx), lambdaHandler) if err != nil { response := httpjson.NewErrorResponse(ctx, err, nil) w.WriteHeader(response.Status) _, _ = w.Write(response.Body) + return } + + writeJSON(w, http.StatusOK, map[string]any{}) default: + fmt.Println(r.Method, r.URL.Path, "- not found") w.WriteHeader(http.StatusNotFound) } }), @@ -269,6 +299,42 @@ func Run(ctx context.Context, opts *RunnerOpts) error { _ = runtimeServer.Shutdown(ctx) }() + // Small sleep to make sure the server has started as runtime.New will start making requests to it + time.Sleep(time.Millisecond * 200) + + // We need to set these as even though we are using a custom endpoint and mocking requests the AWS clients + // still expect to be able to send auth headers, and to do that they read these values from the env. Running locally + // you might just have these set so it's ok, but in CI they are not available and tests fail + os.Setenv("AWS_ACCESS_KEY_ID", "test") + os.Setenv("AWS_SECRET_ACCESS_KEY", "test") + os.Setenv("AWS_SESSION_TOKEN", "test") + os.Setenv("AWS_REGION", "test") + + lambdaHandler, err = runtime.New(ctx, &runtime.HandlerArgs{ + LogLevel: "warn", + SchemaPath: path.Join(opts.Dir, ".build/runtime/schema.json"), + ConfigPath: path.Join(opts.Dir, ".build/runtime/config.json"), + ProjectName: opts.TestGroupName, + Env: "test", + QueueURL: "https://testing-sqs-queue.com/123456789/events", + FunctionsARN: functionsARN, + BucketName: bucketName, + SecretNames: []string{ + "KEEL_DATABASE_URL", + }, + JobsWebhookURL: fmt.Sprintf("%s%s", serverURL, JobsWebhookPath), + + // Send all AWS API calls to our test server + AWSEndpoint: fmt.Sprintf("%s/aws", serverURL), + }) + if err != nil { + fmt.Println("error creating lambda runtime handler:", err) + return err + } + defer func() { + _ = lambdaHandler.Stop() + }() + cmd := exec.Command("./node_modules/.bin/tsc", "--noEmit", "--pretty") cmd.Dir = opts.Dir cmd.Stdout = os.Stdout @@ -283,101 +349,184 @@ func Run(ctx context.Context, opts *RunnerOpts) error { opts.Pattern = "(.*)" } - pk, _ := testhelpers.GetEmbeddedPrivateKey() - - pkBytes := x509.MarshalPKCS1PrivateKey(pk) - pkPem := pem.EncodeToMemory( - &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: pkBytes, - }, - ) - - pkBase64 := base64.StdEncoding.EncodeToString(pkPem) - cmd = exec.Command("./node_modules/.bin/vitest", "run", "--color", "--reporter", "verbose", "--config", "./.build/vitest.config.mjs", "--testNamePattern", opts.Pattern) cmd.Dir = opts.Dir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = append(os.Environ(), []string{ - fmt.Sprintf("KEEL_TESTING_ACTIONS_API_URL=http://localhost:%s/%s/json", runtimePort, ActionApiPath), - fmt.Sprintf("KEEL_TESTING_JOBS_URL=http://localhost:%s/%s/json", runtimePort, JobPath), - fmt.Sprintf("KEEL_TESTING_SUBSCRIBERS_URL=http://localhost:%s/%s/json", runtimePort, SubscriberPath), - fmt.Sprintf("KEEL_TESTING_CLIENT_API_URL=http://localhost:%s/%s", runtimePort, ActionApiPath), - fmt.Sprintf("KEEL_TESTING_AUTH_API_URL=http://localhost:%s/auth", runtimePort), + fmt.Sprintf("KEEL_TESTING_ACTIONS_API_URL=%s/%s/json", serverURL, ActionApiPath), + fmt.Sprintf("KEEL_TESTING_JOBS_URL=%s/%s/json", serverURL, JobPath), + fmt.Sprintf("KEEL_TESTING_SUBSCRIBERS_URL=%s/%s/json", serverURL, SubscriberPath), + fmt.Sprintf("KEEL_TESTING_CLIENT_API_URL=%s/%s", serverURL, ActionApiPath), + fmt.Sprintf("KEEL_TESTING_AUTH_API_URL=%s/auth", serverURL), "KEEL_DB_CONN_TYPE=pg", fmt.Sprintf("KEEL_DB_CONN=%s", dbConnString), // Disables experimental fetch warning that pollutes console experience when running tests "NODE_NO_WARNINGS=1", fmt.Sprintf("KEEL_DEFAULT_PK=%s", pkBase64), + + // Need to set these so the sdk uses the test endpoint in tests + fmt.Sprintf("TEST_AWS_ENDPOINT=%s/aws", serverURL), + fmt.Sprintf("KEEL_FILES_BUCKET_NAME=%s", bucketName), }...) return cmd.Run() } +func writeJSON(w http.ResponseWriter, status int, body any) { + b, _ := json.Marshal(body) + w.WriteHeader(status) + w.Header().Set("content-type", "application/json") + _, _ = w.Write(b) +} + // HandleJobExecutorRequest handles requests the job module in the testing package. -func HandleJobExecutorRequest(ctx context.Context, schema *proto.Schema, jobName string, r *http.Request) error { - body, err := io.ReadAll(r.Body) - if err != nil { - return err - } +func HandleJobExecutorRequest(r *http.Request, h *runtime.Handler, awsHandler *AWSAPIHandler) error { + id := ksuid.New().String() + key := fmt.Sprintf("jobs/%s", id) - identity, err := actions.HandleAuthorizationHeader(ctx, schema, r.Header) + b, err := io.ReadAll(r.Body) if err != nil { return err } - if identity != nil { - ctx = auth.WithIdentity(ctx, identity) + awsHandler.S3Bucket[key] = &S3Object{ + Headers: map[string][]string{ + "content-type": {"application/json"}, + }, + Data: b, } - // handle any Time-Zone headers - location, err := locale.HandleTimezoneHeader(ctx, r.Header) - if err != nil { - return err - } - ctx = locale.WithTimeLocation(ctx, location) - - var inputs map[string]any - // if no json body has been sent, just return an empty map for the inputs - if string(body) == "" { - inputs = nil - } else { - err = json.Unmarshal(body, &inputs) - if err != nil { - return err + token := "" + header := r.Header.Get("Authorization") + if header != "" { + authParts := strings.Split(header, "Bearer ") + if len(authParts) == 2 { + token = authParts[1] } } - trigger := functions.TriggerType(r.Header.Get("X-Trigger-Type")) - - err = runtime.NewJobHandler(schema).RunJob(ctx, jobName, inputs, trigger) + name := path.Base(r.URL.Path) + name = strcase.ToCamel(name) - if err != nil { - return err - } - - return nil + return h.JobHandler(r.Context(), &runtime.RunJobPayload{ + ID: id, + Name: name, + Token: token, + }) } // HandleSubscriberExecutorRequest handles requests the subscriber module in the testing package. -func HandleSubscriberExecutorRequest(ctx context.Context, schema *proto.Schema, subscriberName string, r *http.Request) error { - body, err := io.ReadAll(r.Body) +func HandleSubscriberExecutorRequest(r *http.Request, h *runtime.Handler) error { + b, err := io.ReadAll(r.Body) if err != nil { return err } var event *events.Event - err = json.Unmarshal(body, &event) + err = json.Unmarshal(b, &event) if err != nil { return err } - err = runtime.NewSubscriberHandler(schema).RunSubscriber(ctx, subscriberName, event) - + b, err = json.Marshal(runtime.EventPayload{ + Subscriber: path.Base(r.URL.Path), + Event: event, + Traceparent: "1234", + }) if err != nil { return err } - return nil + return h.EventHandler(r.Context(), lambdaevents.SQSEvent{ + Records: []lambdaevents.SQSMessage{ + { + MessageId: "", + Body: string(b), + }, + }, + }) } + +func toLambdaFunctionURLRequest(r *http.Request) (lambdaevents.LambdaFunctionURLRequest, error) { + headers := make(map[string]string) + for key, values := range r.Header { + if len(values) > 0 { + headers[key] = values[0] + } + } + + queryStringParameters := make(map[string]string) + for key, values := range r.URL.Query() { + if len(values) > 0 { + queryStringParameters[key] = values[0] + } + } + + var body string + if r.Body != nil { + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + return lambdaevents.LambdaFunctionURLRequest{}, fmt.Errorf("failed to read request body: %w", err) + } + body = string(bodyBytes) + // Reset the body for future use + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + } + + return lambdaevents.LambdaFunctionURLRequest{ + Version: "2.0", + RawPath: r.URL.Path, + RawQueryString: r.URL.RawQuery, + Headers: headers, + QueryStringParameters: queryStringParameters, + RequestContext: lambdaevents.LambdaFunctionURLRequestContext{ + HTTP: lambdaevents.LambdaFunctionURLRequestContextHTTPDescription{ + Method: r.Method, + Path: r.URL.Path, + Protocol: r.Proto, + SourceIP: r.RemoteAddr, + UserAgent: r.UserAgent(), + }, + }, + Body: body, + IsBase64Encoded: false, + }, nil +} + +const functionsServerEntry = ` +const { createServer } = require("node:http"); +const { handler } = require("./functions/main.js"); + +const server = createServer(async (req, res) => { + try { + const u = new URL(req.url, "http://" + req.headers.host); + if (req.method === "GET" && u.pathname === "/_health") { + res.statusCode = 200; + res.end(); + return; + } + + const buffers = []; + for await (const chunk of req) { + buffers.push(chunk); + } + const data = Buffer.concat(buffers).toString(); + const json = JSON.parse(data); + + const rpcResponse = await handler(json, {}); + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.write(JSON.stringify(rpcResponse)); + res.end(); + } catch (err) { + res.status = 400; + res.write(err.message); + } + + res.end(); +}); + +const port = (process.env.PORT && parseInt(process.env.PORT, 10)) || 3001; +server.listen(port); +`