From e9625eba309c3cfaca4cc6049b5950308f678c3b Mon Sep 17 00:00:00 2001 From: Brandon Sprague Date: Tue, 3 Oct 2023 16:26:04 -0700 Subject: [PATCH] Add basic async task support to PACTA Add a new `runner` binary, which will be invoked either locally (via Docker) or remotely (via `aztasks`/Azure Container Apps Jobs). Currently, none of the PACTA-specific handling is here, like passing portfolio information, loading blobs, etc, it's just the basic infra. Signed-off-by: Brandon Sprague --- WORKSPACE | 10 +- azure/azlog/BUILD.bazel | 12 ++ azure/azlog/azlog.go | 79 +++++++++ azure/aztask/BUILD.bazel | 15 ++ azure/aztask/aztask.go | 247 ++++++++++++++++++++++++++++ cmd/runner/BUILD.bazel | 68 ++++++++ cmd/runner/README.md | 14 ++ cmd/runner/configs/dev.conf | 2 + cmd/runner/configs/local.conf | 2 + cmd/runner/main.go | 68 ++++++++ cmd/runner/taskrunner/BUILD.bazel | 13 ++ cmd/runner/taskrunner/taskrunner.go | 43 +++++ cmd/server/BUILD.bazel | 8 + cmd/server/main.go | 114 ++++++++++++- cmd/server/pactasrv/BUILD.bazel | 1 + cmd/server/pactasrv/pactasrv.go | 8 +- deps.bzl | 70 +++++++- executors/docker/BUILD.bazel | 17 ++ executors/docker/docker.go | 110 +++++++++++++ executors/local/BUILD.bazel | 9 + executors/local/local.go | 33 ++++ go.mod | 21 +++ go.sum | 52 ++++++ pacta/pacta.go | 6 + secrets/secrets.go | 107 ++++++++++++ task/BUILD.bazel | 9 + task/task.go | 12 ++ 27 files changed, 1139 insertions(+), 11 deletions(-) create mode 100644 azure/azlog/BUILD.bazel create mode 100644 azure/azlog/azlog.go create mode 100644 azure/aztask/BUILD.bazel create mode 100644 azure/aztask/aztask.go create mode 100644 cmd/runner/BUILD.bazel create mode 100644 cmd/runner/README.md create mode 100644 cmd/runner/configs/dev.conf create mode 100644 cmd/runner/configs/local.conf create mode 100644 cmd/runner/main.go create mode 100644 cmd/runner/taskrunner/BUILD.bazel create mode 100644 cmd/runner/taskrunner/taskrunner.go create mode 100644 executors/docker/BUILD.bazel create mode 100644 executors/docker/docker.go create mode 100644 executors/local/BUILD.bazel create mode 100644 executors/local/local.go create mode 100644 task/BUILD.bazel create mode 100644 task/task.go diff --git a/WORKSPACE b/WORKSPACE index 75278c5..827000a 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -94,7 +94,15 @@ load("@rules_oci//oci:pull.bzl", "oci_pull") oci_pull( name = "distroless_base", - digest = "sha256:73deaaf6a207c1a33850257ba74e0f196bc418636cada9943a03d7abea980d6d", + digest = "sha256:46c5b9bd3e3efff512e28350766b54355fce6337a0b44ba3f822ab918eca4520", image = "gcr.io/distroless/base", platforms = ["linux/amd64"], ) + +# TODO: Replace this with the base image provided by RMI +oci_pull( + name = "runner_base", + digest = "sha256:46c5b9bd3e3efff512e28350766b54355fce6337a0b44ba3f822ab918eca4520", + image = "gcr.io/distroless/base", + platforms = ["linux/amd64"], +) \ No newline at end of file diff --git a/azure/azlog/BUILD.bazel b/azure/azlog/BUILD.bazel new file mode 100644 index 0000000..29026e9 --- /dev/null +++ b/azure/azlog/BUILD.bazel @@ -0,0 +1,12 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "azlog", + srcs = ["azlog.go"], + importpath = "github.com/RMI/pacta/azure/azlog", + visibility = ["//visibility:public"], + deps = [ + "@org_uber_go_zap//:zap", + "@org_uber_go_zap//zapcore", + ], +) diff --git a/azure/azlog/azlog.go b/azure/azlog/azlog.go new file mode 100644 index 0000000..6f18436 --- /dev/null +++ b/azure/azlog/azlog.go @@ -0,0 +1,79 @@ +// Package azlog provides a thin wrapper around the zap logging package to +// enable structured logging on Azure. This is still a work in progress and +// is untested. +package azlog + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type Config struct { + Local bool + MinLogLevel zapcore.Level +} + +func New(cfg *Config) (*zap.Logger, error) { + if cfg.Local { + zCfg := zap.NewDevelopmentConfig() + zCfg.Level = zap.NewAtomicLevelAt(cfg.MinLogLevel) + return zCfg.Build() + } + + zCfg := &zap.Config{ + Level: zap.NewAtomicLevelAt(cfg.MinLogLevel), + Development: false, + Sampling: &zap.SamplingConfig{ + Initial: 100, + Thereafter: 100, + }, + Encoding: "json", + EncoderConfig: zapcore.EncoderConfig{ + TimeKey: "time", + LevelKey: "severity", + NameKey: "logger", + CallerKey: "caller", + FunctionKey: zapcore.OmitKey, + MessageKey: "message", + StacktraceKey: "stacktrace", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: encodeLevel, + EncodeTime: zapcore.RFC3339TimeEncoder, + EncodeDuration: zapcore.MillisDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + }, + OutputPaths: []string{"stdout"}, + ErrorOutputPaths: []string{"stderr"}, + } + return zCfg.Build(zap.AddStacktrace(zap.DPanicLevel)) +} + +func encodeLevel(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { + /* + TODO: Figure out what the appropriate logging method is here. + See https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel?view=dotnet-plat-ext-7.0 + Critical 5 Logs that describe an unrecoverable application or system crash, or a catastrophic failure that requires immediate attention. + Debug 1 Logs that are used for interactive investigation during development. These logs should primarily contain information useful for debugging and have no long-term value. + Error 4 Logs that highlight when the current flow of execution is stopped due to a failure. These should indicate a failure in the current activity, not an application-wide failure. + Information 2 Logs that track the general flow of the application. These logs should have long-term value. + None 6 Not used for writing log messages. Specifies that a logging category should not write any messages. + Trace 0 Logs that contain the most detailed messages. These messages may contain sensitive application data. These messages are disabled by default and should never be enabled in a production environment. + Warning 3 Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the application execution to stop. + */ + switch l { + case zapcore.DebugLevel: + enc.AppendString("DEBUG") + case zapcore.InfoLevel: + enc.AppendString("INFO") + case zapcore.WarnLevel: + enc.AppendString("WARNING") + case zapcore.ErrorLevel: + enc.AppendString("ERROR") + case zapcore.DPanicLevel: + enc.AppendString("CRITICAL") + case zapcore.PanicLevel: + enc.AppendString("ALERT") + case zapcore.FatalLevel: + enc.AppendString("EMERGENCY") + } +} diff --git a/azure/aztask/BUILD.bazel b/azure/aztask/BUILD.bazel new file mode 100644 index 0000000..c5a5b08 --- /dev/null +++ b/azure/aztask/BUILD.bazel @@ -0,0 +1,15 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "aztask", + srcs = ["aztask.go"], + importpath = "github.com/RMI/pacta/azure/aztask", + visibility = ["//visibility:public"], + deps = [ + "//task", + "@com_github_azure_azure_sdk_for_go_sdk_azcore//:azcore", + "@com_github_azure_azure_sdk_for_go_sdk_azcore//to", + "@com_github_azure_azure_sdk_for_go_sdk_resourcemanager_appcontainers_armappcontainers_v2//:armappcontainers", + "@com_github_silicon_ally_idgen//:idgen", + ], +) diff --git a/azure/aztask/aztask.go b/azure/aztask/aztask.go new file mode 100644 index 0000000..18a620d --- /dev/null +++ b/azure/aztask/aztask.go @@ -0,0 +1,247 @@ +// Package aztask wraps Azure's Container Apps Jobs API to provide basic async +// task execution for the PACTA ecosystem. +package aztask + +import ( + "bytes" + "context" + "errors" + "fmt" + "math/rand" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + armappcontainers "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v2" + "github.com/RMI/pacta/task" + "github.com/Silicon-Ally/idgen" +) + +type Runner struct { + client *armappcontainers.JobsClient + + cfg *Config + gen *idgen.Generator +} + +type Config struct { + // Location is the location to run the runner, like centralus + Location string + + // ConfigPath should be a full path to a config file in the runner image, + // like: /configs/{local,dev}.conf + ConfigPath string + + // Identity is the account the runner should act as. + Identity *RunnerIdentity + + // Image the runner image to execute + Image *RunnerImage + + Rand *rand.Rand +} + +func (c *Config) validate() error { + if c.Location == "" { + return errors.New("no container location given") + } + + if c.ConfigPath == "" { + return errors.New("no runner config path given") + } + + if err := c.Identity.validate(); err != nil { + return fmt.Errorf("invalid identity config: %w", err) + } + + if err := c.Image.validate(); err != nil { + return fmt.Errorf("invalid image config: %w", err) + } + + return nil +} + +type RunnerIdentity struct { + // Like runner-local + Name string + // Like aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee + SubscriptionID string + // Like rmi-pacta-{local,dev} + ResourceGroup string + // Like ffffffff-0000-1111-2222-333333333333 + ClientID string + // Like pacta-{local,dev}, the name of the Container Apps Environment + ManagedEnvironment string +} + +func (ri *RunnerIdentity) validate() error { + if ri.Name == "" { + return errors.New("no identity name given") + } + if ri.SubscriptionID == "" { + return errors.New("no identity subscription ID given") + } + if ri.ResourceGroup == "" { + return errors.New("no identity resource group given") + } + if ri.ClientID == "" { + return errors.New("no identity client ID given") + } + return nil +} + +func (r *RunnerIdentity) String() string { + tmpl := "/subscriptions/%s/resourcegroups/%s/providers/Microsoft.ManagedIdentity/userAssignedIdentities/%s" + return fmt.Sprintf(tmpl, r.SubscriptionID, r.ResourceGroup, r.Name) +} + +func (r *RunnerIdentity) EnvironmentID() string { + tmpl := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.App/managedEnvironments/%s" + return fmt.Sprintf(tmpl, r.SubscriptionID, r.ResourceGroup, r.ManagedEnvironment) +} + +type RunnerImage struct { + // Like rmipacta.azurecr.io + Registry string + // Like runner + Name string +} + +func (ri *RunnerImage) validate() error { + if ri.Registry == "" { + return errors.New("no runner image registry given") + } + if ri.Name == "" { + return errors.New("no runner image name given") + } + return nil +} + +func (r *RunnerImage) WithTag(tag string) string { + var buf bytes.Buffer + // /: + buf.WriteString(r.Registry) + buf.WriteRune('/') + buf.WriteString(r.Name) + buf.WriteRune(':') + buf.WriteString(tag) + return buf.String() +} + +func NewTaskRunner(creds azcore.TokenCredential, cfg *Config) (*Runner, error) { + if err := cfg.validate(); err != nil { + return nil, fmt.Errorf("invalid task runner config: %w", err) + } + + clientFactory, err := armappcontainers.NewClientFactory(cfg.Identity.SubscriptionID, creds, nil) + if err != nil { + return nil, fmt.Errorf("failed to create client: %w", err) + } + + gen, err := idgen.New(cfg.Rand, idgen.WithDefaultLength(32), idgen.WithCharSet([]rune("abcdefghijklmnopqrstuvwxyz"))) + if err != nil { + return nil, fmt.Errorf("failed to init ID generator: %w", err) + } + + return &Runner{ + client: clientFactory.NewJobsClient(), + cfg: cfg, + gen: gen, + }, nil +} + +func (r *Runner) StartRun(ctx context.Context, req *task.StartRunRequest) (task.ID, error) { + name := r.gen.NewID() + identity := r.cfg.Identity.String() + envID := r.cfg.Identity.EnvironmentID() + + job := armappcontainers.Job{ + Location: &r.cfg.Location, + Identity: &armappcontainers.ManagedServiceIdentity{ + Type: to.Ptr(armappcontainers.ManagedServiceIdentityTypeUserAssigned), + UserAssignedIdentities: map[string]*armappcontainers.UserAssignedIdentity{ + identity: {}, + }, + }, + Properties: &armappcontainers.JobProperties{ + Configuration: &armappcontainers.JobConfiguration{ + ReplicaTimeout: to.Ptr(int32(60 * 60 * 2 /* two hours */)), + TriggerType: to.Ptr(armappcontainers.TriggerTypeManual), + ManualTriggerConfig: &armappcontainers.JobConfigurationManualTriggerConfig{ + // Run one copy. + Parallelism: to.Ptr(int32(1)), + ReplicaCompletionCount: to.Ptr(int32(1)), + }, + // Don't retry, if it failed once, it'll probably fail again. We might relax + // this in the future if we identify "transient" errors. + ReplicaRetryLimit: to.Ptr(int32(0)), + Registries: []*armappcontainers.RegistryCredentials{ + { + Server: to.Ptr(r.cfg.Image.Registry), + Identity: to.Ptr(identity), + }, + }, + Secrets: []*armappcontainers.Secret{ + // TODO: Put any useful configuration here. + }, + }, + EnvironmentID: to.Ptr(envID), + Template: &armappcontainers.JobTemplate{ + Containers: []*armappcontainers.Container{ + { + Args: []*string{ + to.Ptr("--config=" + r.cfg.ConfigPath), + }, + Command: []*string{ + to.Ptr("/runner"), + }, + Env: []*armappcontainers.EnvironmentVar{ + { + Name: to.Ptr("AZURE_CLIENT_ID"), + Value: to.Ptr(r.cfg.Identity.ClientID), + }, + { + Name: to.Ptr("MANAGED_IDENTITY_CLIENT_ID"), + Value: to.Ptr(r.cfg.Identity.ClientID), + }, + { + Name: to.Ptr("PORTFOLIO_ID"), + Value: to.Ptr(string(req.PortfolioID)), + }, + }, + Image: to.Ptr(r.cfg.Image.WithTag("latest")), + Name: to.Ptr(name), + Probes: []*armappcontainers.ContainerAppProbe{}, + Resources: &armappcontainers.ContainerResources{ + CPU: to.Ptr(1.0), + Memory: to.Ptr("2Gi"), + }, + VolumeMounts: []*armappcontainers.VolumeMount{}, + }, + }, + Volumes: []*armappcontainers.Volume{ + // TODO: Mount any sources here. + }, + }, + }, + Tags: map[string]*string{}, + } + poller, err := r.client.BeginCreateOrUpdate(ctx, r.cfg.Identity.ResourceGroup, name, job, nil) + if err != nil { + return "", fmt.Errorf("failed to create container app job: %w", err) + } + + res, err := poller.PollUntilDone(ctx, nil) + if err != nil { + return "", fmt.Errorf("failed to poll container group creation: %w", err) + } + + poller2, err := r.client.BeginStart(ctx, r.cfg.Identity.ResourceGroup, name, nil) + if err != nil { + return "", fmt.Errorf("failed to start container app job: %w", err) + } + if _, err := poller2.PollUntilDone(ctx, nil); err != nil { + return "", fmt.Errorf("failed to poll for container app start: %w", err) + } + + return task.ID(*res.ID), nil +} diff --git a/cmd/runner/BUILD.bazel b/cmd/runner/BUILD.bazel new file mode 100644 index 0000000..5ceddda --- /dev/null +++ b/cmd/runner/BUILD.bazel @@ -0,0 +1,68 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") +load("@rules_pkg//:pkg.bzl", "pkg_tar") +load("@rules_oci//oci:defs.bzl", "oci_image", "oci_push", "oci_tarball") + +go_library( + name = "runner_lib", + srcs = ["main.go"], + importpath = "github.com/RMI/pacta/cmd/runner", + visibility = ["//visibility:private"], + deps = [ + "//azure/azlog", + "//cmd/runner/taskrunner", + "//executors/local", + "//pacta", + "//task", + "@com_github_namsral_flag//:flag", + "@org_uber_go_zap//:zap", + "@org_uber_go_zap//zapcore", + ], +) + +go_binary( + name = "runner", + embed = [":runner_lib"], + visibility = ["//visibility:public"], +) + +pkg_tar( + name = "runner_tar", + srcs = [":runner"], +) + +filegroup( + name = "configs", + srcs = glob(["configs/**"]), + visibility = ["//visibility:public"], +) + +pkg_tar( + name = "configs_tar", + srcs = [":configs"], + package_dir = "/configs", + strip_prefix = "/cmd/runner/configs", +) + +oci_image( + name = "image", + base = "@runner_base", + entrypoint = ["/runner"], + tars = [ + ":runner_tar", + ":configs_tar", + ], +) + +oci_push( + name = "push_image", + image = ":image", + remote_tags = ["latest"], + repository = "rmipacta.azurecr.io/runner", +) + +# Note: This tarball is provided for local testing of the Docker image, see the README.md for details on usage. +oci_tarball( + name = "image_tarball", + image = ":image", + repo_tags = [], +) diff --git a/cmd/runner/README.md b/cmd/runner/README.md new file mode 100644 index 0000000..1b93694 --- /dev/null +++ b/cmd/runner/README.md @@ -0,0 +1,14 @@ +# Runner + +This directory contains the `runner` binary, which acts as a thin shim around the PACTA portfolio analysis tooling, running tasks created via either Azure Container App Jobs (via the `aztask` package) or local Docker (`localRunner`), loading relevant blobs, and writing relevant outputs. + +## Running locally + +The `runner` binary doesn't need to be run locally in order to test PACTA processing. By default, the backend API server will execute PACTA runs against a local Docker daemon, testing most of the run-handling code in the process (e.g. file handling, task execution, etc). + +If you do want to actually run the full `runner` image on Azure, you can use: + +```bash +# Run the backend, tell it to create tasks as real Azure Container Apps Jobs. +bazel run //scripts:run_apiserver -- --use_azure_runner +``` diff --git a/cmd/runner/configs/dev.conf b/cmd/runner/configs/dev.conf new file mode 100644 index 0000000..f438a3b --- /dev/null +++ b/cmd/runner/configs/dev.conf @@ -0,0 +1,2 @@ +env dev +min_log_level warn diff --git a/cmd/runner/configs/local.conf b/cmd/runner/configs/local.conf new file mode 100644 index 0000000..c8b8b00 --- /dev/null +++ b/cmd/runner/configs/local.conf @@ -0,0 +1,2 @@ +env local +min_log_level debug diff --git a/cmd/runner/main.go b/cmd/runner/main.go new file mode 100644 index 0000000..9cd706f --- /dev/null +++ b/cmd/runner/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log" + "os" + + "github.com/RMI/pacta/azure/azlog" + "github.com/RMI/pacta/cmd/runner/taskrunner" + "github.com/RMI/pacta/executors/local" + "github.com/RMI/pacta/pacta" + "github.com/RMI/pacta/task" + "github.com/namsral/flag" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func main() { + if err := run(os.Args); err != nil { + log.Fatal(err) + } +} + +func run(args []string) error { + if len(args) == 0 { + return errors.New("args cannot be empty") + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + fs := flag.NewFlagSet(args[0], flag.ContinueOnError) + var ( + env = fs.String("env", "", "The environment we're running in.") + + minLogLevel zapcore.Level = zapcore.WarnLevel + ) + fs.Var(&minLogLevel, "min_log_level", "If set, retains logs at the given level and above. Options: 'debug', 'info', 'warn', 'error', 'dpanic', 'panic', 'fatal' - default warn.") + + // Allows for passing in configuration via a -config path/to/env-file.conf + // flag, see https://pkg.go.dev/github.com/namsral/flag#readme-usage + fs.String(flag.DefaultConfigFlagname, "", "path to config file") + if err := fs.Parse(args[1:]); err != nil { + return fmt.Errorf("failed to parse flags: %v", err) + } + + logger, err := azlog.New(&azlog.Config{ + Local: *env == "local", + MinLogLevel: minLogLevel, + }) + if err != nil { + return fmt.Errorf("failed to init logger: %w", err) + } + + h := taskrunner.New(&local.Executor{}, logger) + + portfolioID := pacta.PortfolioID(os.Getenv("PORTFOLIO_ID")) + logger.Info("running PACTA task", zap.String("portfolio_id", string(portfolioID))) + if err := h.Execute(ctx, &task.StartRunRequest{ + PortfolioID: portfolioID, + }); err != nil { + return fmt.Errorf("failed to run task: %w", err) + } + + return nil +} diff --git a/cmd/runner/taskrunner/BUILD.bazel b/cmd/runner/taskrunner/BUILD.bazel new file mode 100644 index 0000000..6e9e6ee --- /dev/null +++ b/cmd/runner/taskrunner/BUILD.bazel @@ -0,0 +1,13 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "taskrunner", + srcs = ["taskrunner.go"], + importpath = "github.com/RMI/pacta/cmd/runner/taskrunner", + visibility = ["//visibility:public"], + deps = [ + "//pacta", + "//task", + "@org_uber_go_zap//:zap", + ], +) diff --git a/cmd/runner/taskrunner/taskrunner.go b/cmd/runner/taskrunner/taskrunner.go new file mode 100644 index 0000000..d80d6bf --- /dev/null +++ b/cmd/runner/taskrunner/taskrunner.go @@ -0,0 +1,43 @@ +// Package taskrunner implements the logic for preparing a portfolio for +// analysis, regardless of the underlying substrate we'll run the external +// processing logic on (e.g Docker or locally). +package taskrunner + +import ( + "context" + "fmt" + + "github.com/RMI/pacta/pacta" + "github.com/RMI/pacta/task" + "go.uber.org/zap" +) + +type Executor interface { + // TODO: Update this interface to include relevant inputs and outputs + ProcessPortfolio(ctx context.Context) (*pacta.PortfolioResult, error) +} + +type Handler struct { + logger *zap.Logger + executor Executor +} + +func New(executor Executor, logger *zap.Logger) *Handler { + return &Handler{ + logger: logger, + executor: executor, + } +} + +func (h *Handler) Execute(ctx context.Context, req *task.StartRunRequest) error { + // TODO: Add logic for loading portfolio blobs and putting them in the right places. + + res, err := h.executor.ProcessPortfolio(ctx) + if err != nil { + return fmt.Errorf("failed to process portfolio: %w", err) + } + + h.logger.Info("processed portfolio, result", zap.Any("result", res)) + + return nil +} diff --git a/cmd/server/BUILD.bazel b/cmd/server/BUILD.bazel index 45bdba3..853b6a6 100644 --- a/cmd/server/BUILD.bazel +++ b/cmd/server/BUILD.bazel @@ -8,12 +8,19 @@ go_library( importpath = "github.com/RMI/pacta/cmd/server", visibility = ["//visibility:private"], deps = [ + "//azure/aztask", + "//cmd/runner/taskrunner", "//cmd/server/pactasrv", "//db/sqldb", + "//executors/docker", "//oapierr", "//openapi:pacta_generated", "//secrets", + "//task", + "@com_github_azure_azure_sdk_for_go_sdk_azcore//:azcore", + "@com_github_azure_azure_sdk_for_go_sdk_azidentity//:azidentity", "@com_github_deepmap_oapi_codegen//pkg/chi-middleware", + "@com_github_docker_docker//client", "@com_github_go_chi_chi_v5//:chi", "@com_github_go_chi_chi_v5//middleware", "@com_github_go_chi_httprate//:httprate", @@ -22,6 +29,7 @@ go_library( "@com_github_lestrrat_go_jwx_v2//jwk", "@com_github_namsral_flag//:flag", "@com_github_rs_cors//:cors", + "@com_github_silicon_ally_cryptorand//:cryptorand", "@com_github_silicon_ally_zaphttplog//:zaphttplog", "@org_uber_go_zap//:zap", ], diff --git a/cmd/server/main.go b/cmd/server/main.go index eaf65d9..d619352 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -6,16 +6,26 @@ import ( "errors" "fmt" "log" + "math/rand" "net/http" "os" + "strconv" "time" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/RMI/pacta/azure/aztask" + "github.com/RMI/pacta/cmd/runner/taskrunner" "github.com/RMI/pacta/cmd/server/pactasrv" "github.com/RMI/pacta/db/sqldb" + "github.com/RMI/pacta/executors/docker" "github.com/RMI/pacta/oapierr" oapipacta "github.com/RMI/pacta/openapi/pacta" "github.com/RMI/pacta/secrets" + "github.com/RMI/pacta/task" + "github.com/Silicon-Ally/cryptorand" "github.com/Silicon-Ally/zaphttplog" + "github.com/docker/docker/client" "github.com/go-chi/chi/v5" "github.com/go-chi/httprate" "github.com/go-chi/jwtauth/v5" @@ -52,6 +62,9 @@ func run(args []string) error { env = fs.String("env", "", "The environment that we're running in.") localDSN = fs.String("local_dsn", "", "If set, override the DB addresses retrieved from the secret configuration. Can only be used when running locally.") + // PACTA Execution + useAZRunner = fs.Bool("use_azure_runner", false, "If true, execute PACTA on Azure Container Apps Jobs instead of a local instance.") + // Secrets pgHost = fs.String("secret_postgres_host", "", "Host of the Postgres server, like db.example.com") pgPort = fs.Int("secret_postgres_port", 5432, "Port to connect to the Postgres server on") @@ -61,6 +74,18 @@ func run(args []string) error { authKeyID = fs.String("secret_auth_public_key_id", "", "Key ID (kid) of the JWT tokens to allow") authKeyData = fs.String("secret_auth_public_key_data", "", "PEM-encoded Ed25519 public key to verify JWT tokens with, contains literal \\n characters that will need to be replaced before parsing") + + runnerConfigLocation = fs.String("secret_runner_config_location", "", "Location (like 'centralus') where the runner jobs should be executed") + runnerConfigConfigPath = fs.String("secret_runner_config_config_path", "", "Config path (like '/configs/dev.conf') where the runner jobs should read their base config from") + + runnerConfigIdentityName = fs.String("secret_runner_config_identity_name", "", "Name of the Azure identity to run runner jobs with") + runnerConfigIdentitySubscriptionID = fs.String("secret_runner_config_identity_subscription_id", "", "Subscription ID of the identity to run runner jobs with") + runnerConfigIdentityResourceGroup = fs.String("secret_runner_config_identity_resource_group", "", "Resource group of the identity to run runner jobs with") + runnerConfigIdentityClientID = fs.String("secret_runner_config_identity_client_id", "", "Client ID of the identity to run runner jobs with") + runnerConfigIdentityManagedEnvironment = fs.String("secret_runner_config_identity_managed_environment", "", "Name of the Container Apps Environment where runner jobs should run") + + runnerConfigImageRegistry = fs.String("secret_runner_config_image_registry", "", "Registry where PACTA runner images live, like 'rmipacta.azurecr.io'") + runnerConfigImageName = fs.String("secret_runner_config_image_name", "", "Name of the Docker image of the PACTA runner, like 'runner'") ) // Allows for passing in configuration via a -config path/to/env-file.conf // flag, see https://pkg.go.dev/github.com/namsral/flag#readme-usage @@ -98,10 +123,26 @@ func run(args []string) error { ID: *authKeyID, Data: *authKeyData, }, + RunnerConfig: &secrets.RawRunnerConfig{ + Location: *runnerConfigLocation, + ConfigPath: *runnerConfigConfigPath, + Identity: &secrets.RawRunnerIdentity{ + Name: *runnerConfigIdentityName, + SubscriptionID: *runnerConfigIdentitySubscriptionID, + ResourceGroup: *runnerConfigIdentityResourceGroup, + ClientID: *runnerConfigIdentityClientID, + ManagedEnvironment: *runnerConfigIdentityManagedEnvironment, + }, + Image: &secrets.RawRunnerImage{ + Registry: *runnerConfigImageRegistry, + Name: *runnerConfigImageName, + }, + }, }) if err != nil { return fmt.Errorf("failed to parse secrets: %w", err) } + runCfg := sec.RunnerConfig if *localDSN != "" && *env != "local" { return errors.New("--local_dsn set outside of local environment") @@ -140,9 +181,62 @@ func run(args []string) error { // that server names match. We don't know how this thing will be run. pactaSwagger.Servers = nil + var creds azcore.TokenCredential + // This is necessary because the default timeout is too low in + // azidentity.NewDefaultAzureCredentials, so it times out and fails to run. + if azClientID := os.Getenv("AZURE_CLIENT_ID"); azClientID != "" { + logger.Info("Loading user managed credentials", zap.String("client_id", azClientID)) + if creds, err = azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ + ID: azidentity.ClientID(azClientID), + }); err != nil { + return fmt.Errorf("failed to load Azure credentials: %w", err) + } + } else { + logger.Info("Loading default credentials") + if creds, err = azidentity.NewDefaultAzureCredential(nil); err != nil { + return fmt.Errorf("failed to load Azure credentials: %w", err) + } + } + + var taskRunner pactasrv.TaskRunner + if *useAZRunner { + logger.Info("initializing Azure task runner client") + tmp, err := aztask.NewTaskRunner(creds, &aztask.Config{ + Location: runCfg.Location, + ConfigPath: runCfg.ConfigPath, + Rand: rand.New(cryptorand.New()), + Identity: &aztask.RunnerIdentity{ + Name: runCfg.Identity.Name, + SubscriptionID: runCfg.Identity.SubscriptionID, + ResourceGroup: runCfg.Identity.ResourceGroup, + ClientID: runCfg.Identity.ClientID, + ManagedEnvironment: runCfg.Identity.ManagedEnvironment, + }, + Image: &aztask.RunnerImage{ + Registry: runCfg.Image.Registry, + Name: runCfg.Image.Name, + }, + }) + if err != nil { + return fmt.Errorf("failed to init task runner: %w", err) + } + taskRunner = tmp + } else { + cli, err := client.NewEnvClient() + if err != nil { + return fmt.Errorf("failed to initialize Docker client: %w", err) + } + + dockerExec := docker.NewExecutor(cli, logger) + taskRunner = &localRunner{ + handler: taskrunner.New(dockerExec, logger), + } + } + // Create an instance of our handler which satisfies each generated interface srv := &pactasrv.Server{ - DB: db, + DB: db, + TaskRunner: taskRunner, } pactaStrictHandler := oapipacta.NewStrictHandlerWithOptions(srv, nil /* middleware */, oapipacta.StrictHTTPServerOptions{ @@ -276,3 +370,21 @@ func responseErrorHandlerFuncForService(logger *zap.Logger, svc string) func(w h http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } } + +// localRunner is a thin shim around *taskrunner.Handler that satisfies the +// task runner interface, allowing us to run the handler in-process, usually for +// local development. +type localRunner struct { + handler *taskrunner.Handler + + taskID int +} + +func (l *localRunner) StartRun(ctx context.Context, req *task.StartRunRequest) (task.ID, error) { + if err := l.handler.Execute(ctx, req); err != nil { + return "", fmt.Errorf("error from handler: %w", err) + } + + l.taskID++ + return task.ID("task:" + strconv.Itoa(l.taskID)), nil +} diff --git a/cmd/server/pactasrv/BUILD.bazel b/cmd/server/pactasrv/BUILD.bazel index 7957c74..86288b4 100644 --- a/cmd/server/pactasrv/BUILD.bazel +++ b/cmd/server/pactasrv/BUILD.bazel @@ -16,6 +16,7 @@ go_library( "//oapierr", "//openapi:pacta_generated", "//pacta", + "//task", "@com_github_go_chi_jwtauth_v5//:jwtauth", "@org_uber_go_zap//:zap", ], diff --git a/cmd/server/pactasrv/pactasrv.go b/cmd/server/pactasrv/pactasrv.go index 8ee0aac..9e6bf6b 100644 --- a/cmd/server/pactasrv/pactasrv.go +++ b/cmd/server/pactasrv/pactasrv.go @@ -7,6 +7,7 @@ import ( "github.com/RMI/pacta/db" "github.com/RMI/pacta/oapierr" "github.com/RMI/pacta/pacta" + "github.com/RMI/pacta/task" "go.uber.org/zap" ) @@ -15,6 +16,10 @@ var ( invalidEmail = oapierr.ErrorID("invalid_email") ) +type TaskRunner interface { + StartRun(context.Context, *task.StartRunRequest) (task.ID, error) +} + type DB interface { Begin(context.Context) (db.Tx, error) NoTxn(context.Context) db.Tx @@ -66,7 +71,8 @@ type DB interface { } type Server struct { - DB DB + DB DB + TaskRunner TaskRunner } func mapAll[I any, O any](is []I, f func(I) (O, error)) ([]O, error) { diff --git a/deps.bzl b/deps.bzl index 78b2038..c972d05 100644 --- a/deps.bzl +++ b/deps.bzl @@ -173,15 +173,27 @@ def go_dependencies(): go_repository( name = "com_github_azure_azure_sdk_for_go_sdk_azcore", importpath = "github.com/Azure/azure-sdk-for-go/sdk/azcore", - sum = "h1:rTnT/Jrcm+figWlYz4Ixzt0SJVR2cMC8lvZcimipiEY=", - version = "v1.4.0", + sum = "h1:/iHxaJhsFr0+xVFfbMr5vxz848jyiWuIEDhYq3y5odY=", + version = "v1.7.1", + ) + go_repository( + name = "com_github_azure_azure_sdk_for_go_sdk_azidentity", + importpath = "github.com/Azure/azure-sdk-for-go/sdk/azidentity", + sum = "h1:LNHhpdK7hzUcx/k1LIcuh5k7k1LGIWLQfCjaneSj7Fc=", + version = "v1.3.1", ) go_repository( name = "com_github_azure_azure_sdk_for_go_sdk_internal", importpath = "github.com/Azure/azure-sdk-for-go/sdk/internal", - sum = "h1:+5VZ72z0Qan5Bog5C+ZkgSqUbeVUd9wgtHOrIKuc5b8=", - version = "v1.1.2", + sum = "h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=", + version = "v1.3.0", + ) + go_repository( + name = "com_github_azure_azure_sdk_for_go_sdk_resourcemanager_appcontainers_armappcontainers_v2", + importpath = "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v2", + sum = "h1:1PD0CnFSl1m1TCwudP3cIiyTABCWVzHXtYc6Vi5J0JY=", + version = "v2.0.0", ) go_repository( @@ -230,6 +242,12 @@ def go_dependencies(): sum = "h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=", version = "v0.6.0", ) + go_repository( + name = "com_github_azuread_microsoft_authentication_library_for_go", + importpath = "github.com/AzureAD/microsoft-authentication-library-for-go", + sum = "h1:WpB/QDNLpMw72xHJc34BNNykqSOeEJDAWkhf0u12/Jk=", + version = "v1.1.1", + ) go_repository( name = "com_github_bazelbuild_rules_go", @@ -454,6 +472,12 @@ def go_dependencies(): sum = "h1:pJNZnU7uS9MRoYqpoir05B+bCYXrS9sPGE4G1o9EDA8=", version = "v1.2.1", ) + go_repository( + name = "com_github_dnaeon_go_vcr", + importpath = "github.com/dnaeon/go-vcr", + sum = "h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=", + version = "v1.2.0", + ) go_repository( name = "com_github_docker_distribution", @@ -708,8 +732,14 @@ def go_dependencies(): go_repository( name = "com_github_golang_jwt_jwt_v4", importpath = "github.com/golang-jwt/jwt/v4", - sum = "h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=", - version = "v4.4.2", + sum = "h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=", + version = "v4.5.0", + ) + go_repository( + name = "com_github_golang_jwt_jwt_v5", + importpath = "github.com/golang-jwt/jwt/v5", + sum = "h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=", + version = "v5.0.0", ) go_repository( @@ -1153,8 +1183,8 @@ def go_dependencies(): go_repository( name = "com_github_kisielk_errcheck", importpath = "github.com/kisielk/errcheck", - sum = "h1:ZqfnKyx9KGpRcW04j5nnPDgRgoXUeLh2YFBeFzphcA0=", - version = "v1.1.0", + sum = "h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY=", + version = "v1.5.0", ) go_repository( name = "com_github_kisielk_gotool", @@ -1220,6 +1250,12 @@ def go_dependencies(): sum = "h1:C8dUGp0qkwncKtAnozHCbbqhptefzEd1I0sfnuy9rYQ=", version = "v0.6.4", ) + go_repository( + name = "com_github_kylelemons_godebug", + importpath = "github.com/kylelemons/godebug", + sum = "h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=", + version = "v1.1.0", + ) go_repository( name = "com_github_labstack_echo_v4", @@ -1447,6 +1483,12 @@ def go_dependencies(): sum = "h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=", version = "v0.0.0-20170929034955-c48cc78d4826", ) + go_repository( + name = "com_github_montanaflynn_stats", + importpath = "github.com/montanaflynn/stats", + sum = "h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU=", + version = "v0.7.0", + ) go_repository( name = "com_github_morikuni_aec", @@ -1865,6 +1907,12 @@ def go_dependencies(): sum = "h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=", version = "v0.0.0-20181117223130-1be2e3e5546d", ) + go_repository( + name = "com_github_yuin_goldmark", + importpath = "github.com/yuin/goldmark", + sum = "h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=", + version = "v1.4.13", + ) go_repository( name = "com_github_zeebo_xxh3", @@ -2319,3 +2367,9 @@ def go_dependencies(): sum = "h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=", version = "v1.24.0", ) + go_repository( + name = "tools_gotest_v3", + importpath = "gotest.tools/v3", + sum = "h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=", + version = "v3.5.1", + ) diff --git a/executors/docker/BUILD.bazel b/executors/docker/BUILD.bazel new file mode 100644 index 0000000..352d1d7 --- /dev/null +++ b/executors/docker/BUILD.bazel @@ -0,0 +1,17 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "docker", + srcs = ["docker.go"], + importpath = "github.com/RMI/pacta/executors/docker", + visibility = ["//visibility:public"], + deps = [ + "//pacta", + "@com_github_docker_docker//api/types", + "@com_github_docker_docker//api/types/container", + "@com_github_docker_docker//client", + "@com_github_docker_docker//pkg/stdcopy", + "@com_github_opencontainers_image_spec//specs-go/v1:specs-go", + "@org_uber_go_zap//:zap", + ], +) diff --git a/executors/docker/docker.go b/executors/docker/docker.go new file mode 100644 index 0000000..3f240c9 --- /dev/null +++ b/executors/docker/docker.go @@ -0,0 +1,110 @@ +// Package docker implements an executor that runs PACTA processing in a +// container against a local Docker daemon. This implementation is used for +// local testing, where one likely doesn't have a functional portfolio-handling +// environment on the host machine. +package docker + +import ( + "bytes" + "context" + "fmt" + + "github.com/RMI/pacta/pacta" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/stdcopy" + "go.uber.org/zap" + + specs "github.com/opencontainers/image-spec/specs-go/v1" +) + +type Executor struct { + client *client.Client + logger *zap.Logger +} + +func NewExecutor(dockerClient *client.Client, logger *zap.Logger) *Executor { + return &Executor{client: dockerClient, logger: logger} +} + +func (e *Executor) ProcessPortfolio(ctx context.Context) (*pacta.PortfolioResult, error) { + cfg := &container.Config{ + // Use our runner image, which should contain PACTA processing infra. + Image: "rmipacta.azurecr.io/runner:latest", + + // Run the script, tell it to output data to our mounted location. + Cmd: []string{"echo", "TODO, the command to execute"}, + + AttachStdin: false, + Tty: false, + } + + hostCfg := &container.HostConfig{ + // TODO: Add relevant inputs + outputs, likely as mounts + } + platform := &specs.Platform{ + Architecture: "amd64", + OS: "linux", + } + + resp, err := e.client.ContainerCreate(ctx, cfg, hostCfg, nil /* net config */, platform, "" /* random name */) + if err != nil { + return nil, fmt.Errorf("failed to create PACTA container: %w", err) + } + defer func() { + err := e.client.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{ + RemoveVolumes: true, + RemoveLinks: false, + Force: true, + }) + if err != nil { + e.logger.Error("failed to clean up container", + zap.String("container_id", resp.ID), + zap.Error(err)) + } + }() + + if err := e.client.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { + return nil, fmt.Errorf("failed to start PACTA container: %w", err) + } + + waitC, errC := e.client.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) + + select { + case resp := <-waitC: + if resp.Error != nil { + return nil, fmt.Errorf("error in container wait response: %+v", resp.Error) + } + case err := <-errC: + return nil, fmt.Errorf("error while waiting for container to complete: %w", err) + } + + // If we're here, container exited successfully, let's load the logs. + logRC, err := e.client.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Tail: "all", + Details: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to read container logs: %w", err) + } + defer func() { + if err := logRC.Close(); err != nil { + e.logger.Warn("failed to close contrainer log reader", + zap.String("container_id", resp.ID), + zap.Error(err)) + } + }() + + var stdout, stderr bytes.Buffer + if _, err := stdcopy.StdCopy(&stdout, &stderr, logRC); err != nil { + return nil, fmt.Errorf("failed to read logs: %w", err) + } + + return &pacta.PortfolioResult{ + Stdout: stdout.String(), + Stderr: stderr.String(), + }, nil +} diff --git a/executors/local/BUILD.bazel b/executors/local/BUILD.bazel new file mode 100644 index 0000000..43e0ad6 --- /dev/null +++ b/executors/local/BUILD.bazel @@ -0,0 +1,9 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "local", + srcs = ["local.go"], + importpath = "github.com/RMI/pacta/executors/local", + visibility = ["//visibility:public"], + deps = ["//pacta"], +) diff --git a/executors/local/local.go b/executors/local/local.go new file mode 100644 index 0000000..9153e29 --- /dev/null +++ b/executors/local/local.go @@ -0,0 +1,33 @@ +// Package local implements an executor that runs PACTA processing directly +// on the current machine via os/exec. This implementation is used in deployed +// environments, where the runner binary will be baked into a Docker image +// with the portfolio processing code, and so has local access to a functioning +// portfolio-handling environment. +package local + +import ( + "bytes" + "context" + "fmt" + "os/exec" + + "github.com/RMI/pacta/pacta" +) + +type Executor struct{} + +func (e *Executor) ProcessPortfolio(ctx context.Context) (*pacta.PortfolioResult, error) { + var stdout, stderr bytes.Buffer + cmd := exec.CommandContext(ctx, "echo", "TODO, the command to execute") + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("failed to run pacta test CLI: %w", err) + } + + return &pacta.PortfolioResult{ + Stdout: stdout.String(), + Stderr: stderr.String(), + }, nil +} diff --git a/go.mod b/go.mod index 8c692b7..b4acae1 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module github.com/RMI/pacta go 1.20 require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v2 v2.0.0 github.com/Silicon-Ally/cryptorand v1.0.1 github.com/Silicon-Ally/idgen v1.0.1 github.com/Silicon-Ally/testpgx v0.0.4 @@ -10,6 +13,7 @@ require ( github.com/bazelbuild/rules_go v0.41.0 github.com/deepmap/oapi-codegen v1.12.4 github.com/dimuska139/go-email-normalizer v1.2.1 + github.com/docker/docker v20.10.24+incompatible github.com/go-chi/chi/v5 v5.0.10 github.com/go-chi/httprate v0.7.4 github.com/go-chi/jwtauth/v5 v5.1.0 @@ -18,18 +22,27 @@ require ( github.com/jackc/pgx/v5 v5.4.3 github.com/lestrrat-go/jwx/v2 v2.0.6 github.com/namsral/flag v1.7.4-pre + github.com/opencontainers/image-spec v1.0.2 github.com/rs/cors v1.9.0 github.com/spf13/cobra v1.1.3 go.uber.org/zap v1.24.0 ) require ( + github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/getkin/kin-openapi v0.112.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/swag v0.21.1 // indirect github.com/goccy/go-json v0.9.11 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/golang-migrate/migrate/v4 v4.16.2 // indirect github.com/google/uuid v1.3.1 // indirect github.com/gorilla/mux v1.8.0 // indirect @@ -41,6 +54,7 @@ require ( github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect @@ -50,6 +64,9 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -57,9 +74,13 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.13.0 // indirect + golang.org/x/mod v0.10.0 // indirect golang.org/x/net v0.15.0 // indirect golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.12.0 // indirect golang.org/x/text v0.13.0 // indirect + golang.org/x/tools v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.5.1 // indirect ) diff --git a/go.sum b/go.sum index d0b1dc3..108c277 100644 --- a/go.sum +++ b/go.sum @@ -11,10 +11,21 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 h1:/iHxaJhsFr0+xVFfbMr5vxz848jyiWuIEDhYq3y5odY= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1 h1:LNHhpdK7hzUcx/k1LIcuh5k7k1LGIWLQfCjaneSj7Fc= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v2 v2.0.0 h1:1PD0CnFSl1m1TCwudP3cIiyTABCWVzHXtYc6Vi5J0JY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v2 v2.0.0/go.mod h1:xCGT95xV5ei4ahSgJWy31pPGE3xWfaWpr9uRzwTzsmg= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 h1:WpB/QDNLpMw72xHJc34BNNykqSOeEJDAWkhf0u12/Jk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Silicon-Ally/cryptorand v1.0.1 h1:CSZ9dYlY++GN9g/+znhk2qZevjkZ+nSlBJh2rB7jj8A= github.com/Silicon-Ally/cryptorand v1.0.1/go.mod h1:NxU07fWVigUzimVH2WOVanUHcjwvPA9CRpnfS7Cm3/c= @@ -60,10 +71,15 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/dhui/dktest v0.3.16 h1:i6gq2YQEtcrjKbeJpBkWjE8MmLZPYllcjOFbTZuPDnw= github.com/dimuska139/go-email-normalizer v1.2.1 h1:pJNZnU7uS9MRoYqpoir05B+bCYXrS9sPGE4G1o9EDA8= github.com/dimuska139/go-email-normalizer v1.2.1/go.mod h1:fGPWcd/7PSz9aOHusKVYmDk+oKahH/fZTCQ7tTU7e0Y= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= 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 v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= +github.com/docker/docker v20.10.24+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +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/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/getkin/kin-openapi v0.112.0 h1:lnLXx3bAG53EJVI4E/w0N8i1Y/vUZUEsnrXkgnfn7/Y= @@ -90,6 +106,9 @@ github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 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/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA= github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -167,6 +186,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -176,6 +196,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v4 v4.9.1 h1:GliPYSpzGKlyOhqIbG8nmHBo3i1saKWFOgh41AN3b+Y= github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= @@ -225,12 +247,17 @@ github.com/namsral/flag v1.7.4-pre/go.mod h1:OXldTctbM6SWH1K899kPZcf65KxJiD7Msce github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 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/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -287,6 +314,8 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -305,6 +334,8 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf 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= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= @@ -326,7 +357,10 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 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-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -341,6 +375,8 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= @@ -352,6 +388,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -367,21 +405,26 @@ 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-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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -399,8 +442,15 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= 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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -441,6 +491,8 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pacta/pacta.go b/pacta/pacta.go index 44144f6..54d3f17 100644 --- a/pacta/pacta.go +++ b/pacta/pacta.go @@ -705,3 +705,9 @@ func cloneAll[T cloneable[T]](in []T) []T { } return out } + +// TODO: Populate with relevant output from portfolio analysis +type PortfolioResult struct { + Stdout string + Stderr string +} diff --git a/secrets/secrets.go b/secrets/secrets.go index ce1571f..26cab9d 100644 --- a/secrets/secrets.go +++ b/secrets/secrets.go @@ -16,6 +16,7 @@ import ( type PACTAConfig struct { AuthVerificationKey AuthVerificationKey Postgres *pgxpool.Config + RunnerConfig RunnerConfig } type AuthVerificationKey struct { @@ -23,9 +24,30 @@ type AuthVerificationKey struct { PublicKey ed25519.PublicKey } +type RunnerConfig struct { + Location string + ConfigPath string + Identity RunnerIdentity + Image RunnerImage +} + +type RunnerIdentity struct { + Name string + SubscriptionID string + ResourceGroup string + ClientID string + ManagedEnvironment string +} + +type RunnerImage struct { + Registry string + Name string +} + type RawPACTAConfig struct { PostgresConfig *RawPostgresConfig AuthVerificationKey *RawAuthVerificationKey + RunnerConfig *RawRunnerConfig } type RawAuthVerificationKey struct { @@ -33,6 +55,26 @@ type RawAuthVerificationKey struct { Data string } +type RawRunnerConfig struct { + Location string + ConfigPath string + Identity *RawRunnerIdentity + Image *RawRunnerImage +} + +type RawRunnerIdentity struct { + Name string + SubscriptionID string + ResourceGroup string + ClientID string + ManagedEnvironment string +} + +type RawRunnerImage struct { + Registry string + Name string +} + func LoadPACTA(rawCfg *RawPACTAConfig) (*PACTAConfig, error) { if rawCfg == nil { return nil, errors.New("no raw config provided") @@ -48,9 +90,15 @@ func LoadPACTA(rawCfg *RawPACTAConfig) (*PACTAConfig, error) { return nil, fmt.Errorf("failed to parse auth verification key config: %w", err) } + runnerConfig, err := parseRunnerConfig(rawCfg.RunnerConfig) + if err != nil { + return nil, fmt.Errorf("failed to parse runner config: %w", err) + } + return &PACTAConfig{ Postgres: pgxCfg, AuthVerificationKey: authVerificationKey, + RunnerConfig: runnerConfig, }, nil } @@ -154,3 +202,62 @@ func decodePEM(typ string, dat []byte) ([]byte, error) { return block.Bytes, nil } + +func parseRunnerConfig(cfg *RawRunnerConfig) (RunnerConfig, error) { + if cfg == nil { + return RunnerConfig{}, errors.New("no runner config was provided") + } + + if cfg.Location == "" { + return RunnerConfig{}, errors.New("no runner_config.location was provided") + } + + if cfg.Image == nil { + return RunnerConfig{}, errors.New("no runner_config.image was provided") + } + if cfg.Image.Name == "" { + return RunnerConfig{}, errors.New("no runner_config.image.name was provided") + } + if cfg.Image.Registry == "" { + return RunnerConfig{}, errors.New("no runner_config.image.registry was provided") + } + + if cfg.ConfigPath == "" { + return RunnerConfig{}, errors.New("no runner_config.config_path was provided") + } + + if cfg.Identity == nil { + return RunnerConfig{}, errors.New("no runner_config.identity was provided") + } + if cfg.Identity.Name == "" { + return RunnerConfig{}, errors.New("no runner_config.identity.name was provided") + } + if cfg.Identity.SubscriptionID == "" { + return RunnerConfig{}, errors.New("no runner_config.identity.subscription_id was provided") + } + if cfg.Identity.ResourceGroup == "" { + return RunnerConfig{}, errors.New("no runner_config.identity.resource_group was provided") + } + if cfg.Identity.ClientID == "" { + return RunnerConfig{}, errors.New("no runner_config.identity.client_id was provided") + } + if cfg.Identity.ManagedEnvironment == "" { + return RunnerConfig{}, errors.New("no runner_config.identity.managed_environment was provided") + } + + return RunnerConfig{ + Location: cfg.Location, + ConfigPath: cfg.ConfigPath, + Identity: RunnerIdentity{ + Name: cfg.Identity.Name, + SubscriptionID: cfg.Identity.SubscriptionID, + ResourceGroup: cfg.Identity.ResourceGroup, + ClientID: cfg.Identity.ClientID, + ManagedEnvironment: cfg.Identity.ManagedEnvironment, + }, + Image: RunnerImage{ + Registry: cfg.Image.Registry, + Name: cfg.Image.Name, + }, + }, nil +} diff --git a/task/BUILD.bazel b/task/BUILD.bazel new file mode 100644 index 0000000..ca9c691 --- /dev/null +++ b/task/BUILD.bazel @@ -0,0 +1,9 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "task", + srcs = ["task.go"], + importpath = "github.com/RMI/pacta/task", + visibility = ["//visibility:public"], + deps = ["//pacta"], +) diff --git a/task/task.go b/task/task.go new file mode 100644 index 0000000..3f131cf --- /dev/null +++ b/task/task.go @@ -0,0 +1,12 @@ +// Package task holds domain types for asynchronous PACTA work, like analyzing profiles +package task + +import "github.com/RMI/pacta/pacta" + +// TaskID uniquely identifies a task being processed. +type ID string + +type StartRunRequest struct { + // This is just an example, this will likely change over time. + PortfolioID pacta.PortfolioID +}