diff --git a/cmd/tk/args.go b/cmd/tk/args.go index 0a55e2f50..c09d63fb0 100644 --- a/cmd/tk/args.go +++ b/cmd/tk/args.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "os" "path/filepath" @@ -12,37 +13,40 @@ import ( "github.com/grafana/tanka/pkg/tanka" ) -var workflowArgs = cli.Args{ - Validator: cli.ValidateExact(1), - Predictor: cli.PredictFunc(func(args complete.Args) []string { - pwd, err := os.Getwd() - if err != nil { - return nil - } - - root, err := jpath.FindRoot(pwd) - if err != nil { - return nil - } - - envs, err := tanka.FindEnvs(pwd, tanka.FindOpts{}) - if err != nil && !errors.As(err, &tanka.ErrParallel{}) { - return nil - } - - var reldirs []string - for _, env := range envs { - path := filepath.Join(root, env.Metadata.Namespace) // namespace == path on disk - reldir, err := filepath.Rel(pwd, path) - if err == nil { - reldirs = append(reldirs, reldir) +func generateWorkflowArgs(ctx context.Context) cli.Args { + var workflowArgs = cli.Args{ + Validator: cli.ValidateExact(1), + Predictor: cli.PredictFunc(func(args complete.Args) []string { + pwd, err := os.Getwd() + if err != nil { + return nil } - } - if len(reldirs) != 0 { - return reldirs - } + root, err := jpath.FindRoot(pwd) + if err != nil { + return nil + } + + envs, err := tanka.FindEnvs(ctx, pwd, tanka.FindOpts{}) + if err != nil && !errors.As(err, &tanka.ErrParallel{}) { + return nil + } + + var reldirs []string + for _, env := range envs { + path := filepath.Join(root, env.Metadata.Namespace) // namespace == path on disk + reldir, err := filepath.Rel(pwd, path) + if err == nil { + reldirs = append(reldirs, reldir) + } + } + + if len(reldirs) != 0 { + return reldirs + } - return complete.PredictFiles("*").Predict(args) - }), + return complete.PredictFiles("*").Predict(args) + }), + } + return workflowArgs } diff --git a/cmd/tk/env.go b/cmd/tk/env.go index c958c64e7..43b3e1339 100644 --- a/cmd/tk/env.go +++ b/cmd/tk/env.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "fmt" "os" @@ -14,6 +15,7 @@ import ( "github.com/pkg/errors" "github.com/posener/complete" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/jsonnet/jpath" "github.com/grafana/tanka/pkg/kubernetes/client" "github.com/grafana/tanka/pkg/spec/v1alpha1" @@ -21,7 +23,7 @@ import ( "github.com/grafana/tanka/pkg/term" ) -func envCmd() *cli.Command { +func envCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "env [action]", Short: "manipulate environments", @@ -30,10 +32,10 @@ func envCmd() *cli.Command { addCommandsWithLogLevelOption( cmd, - envAddCmd(), - envSetCmd(), - envListCmd(), - envRemoveCmd(), + envAddCmd(ctx), + envSetCmd(ctx), + envListCmd(ctx), + envRemoveCmd(ctx), ) return cmd @@ -46,11 +48,11 @@ var kubectlContexts = cli.PredictFunc( }, ) -func envSetCmd() *cli.Command { +func envSetCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "set ", Short: "update properties of an environment", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), Predictors: complete.Flags{ "server-from-context": kubectlContexts, }, @@ -65,6 +67,8 @@ func envSetCmd() *cli.Command { _ = cmd.Flags().MarkHidden("name") cmd.Run = func(cmd *cli.Command, args []string) error { + _, span := tracer.Start(ctx, "envSetCmd") + defer span.End() if *name != "" { return fmt.Errorf("it looks like you attempted to rename the environment using `--name`. However, this is not possible with Tanka, because the environments name is inferred from the directories name. To rename the environment, rename its directory instead") } @@ -82,7 +86,7 @@ func envSetCmd() *cli.Command { tmp.Spec.APIServer = server } - cfg, err := tanka.Peek(path, tanka.Opts{}) + cfg, err := tanka.Peek(ctx, path, tanka.Opts{}) if err != nil { return err } @@ -119,7 +123,7 @@ func envSetCmd() *cli.Command { return cmd } -func envAddCmd() *cli.Command { +func envAddCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "add ", Short: "create a new environment", @@ -130,6 +134,8 @@ func envAddCmd() *cli.Command { inline := cmd.Flags().BoolP("inline", "i", false, "create an inline environment") cmd.Run = func(cmd *cli.Command, args []string) error { + _, span := tracer.Start(ctx, "envAddCmd") + defer span.End() if cmd.Flags().Changed("server-from-context") { server, err := client.IPFromContext(cfg.Spec.APIServer) if err != nil { @@ -200,13 +206,15 @@ func addEnv(dir string, cfg *v1alpha1.Environment, inline bool) error { return nil } -func envRemoveCmd() *cli.Command { +func envRemoveCmd(ctx context.Context) *cli.Command { return &cli.Command{ Use: "remove ", Aliases: []string{"rm"}, Short: "delete an environment", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), Run: func(_ *cli.Command, args []string) error { + _, span := tracer.Start(ctx, "envRemoveCmd") + defer span.End() for _, arg := range args { path, err := filepath.Abs(arg) if err != nil { @@ -225,8 +233,8 @@ func envRemoveCmd() *cli.Command { } } -func envListCmd() *cli.Command { - args := workflowArgs +func envListCmd(ctx context.Context) *cli.Command { + args := generateWorkflowArgs(ctx) args.Validator = cli.ArgsRange(0, 1) cmd := &cli.Command{ @@ -246,6 +254,9 @@ func envListCmd() *cli.Command { getJsonnetOpts := jsonnetFlags(cmd.Flags()) cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "envListCmd") + defer span.End() + var path string var err error if len(args) == 1 { @@ -257,8 +268,9 @@ func envListCmd() *cli.Command { } } - envs, err := tanka.FindEnvs(path, tanka.FindOpts{JsonnetImplementation: jsonnetImplementation, Selector: getLabelSelector(), JsonnetOpts: getJsonnetOpts()}) + envs, err := tanka.FindEnvs(ctx, path, tanka.FindOpts{JsonnetImplementation: jsonnetImplementation, Selector: getLabelSelector(), JsonnetOpts: getJsonnetOpts()}) if err != nil { + telemetry.FailSpanWithError(span, err) return err } sort.SliceStable(envs, func(i, j int) bool { return envs[i].Metadata.Name < envs[j].Metadata.Name }) @@ -266,7 +278,9 @@ func envListCmd() *cli.Command { if *useJSON { j, err := json.Marshal(envs) if err != nil { - return fmt.Errorf("formatting as json: %s", err) + err = fmt.Errorf("formatting as json: %s", err) + telemetry.FailSpanWithError(span, err) + return err } fmt.Println(string(j)) return nil diff --git a/cmd/tk/export.go b/cmd/tk/export.go index 4d54bb813..b28a4d39f 100644 --- a/cmd/tk/export.go +++ b/cmd/tk/export.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "regexp" @@ -13,8 +14,8 @@ import ( "github.com/grafana/tanka/pkg/tanka" ) -func exportCmd() *cli.Command { - args := workflowArgs +func exportCmd(ctx context.Context) *cli.Command { + args := generateWorkflowArgs(ctx) args.Validator = cli.ArgsMin(2) cmd := &cli.Command{ @@ -49,6 +50,9 @@ func exportCmd() *cli.Command { recursive := cmd.Flags().BoolP("recursive", "r", false, "Look recursively for Tanka environments") cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "exportCmd") + defer span.End() + // Allocate a block of memory to alter GC behaviour. See https://github.com/golang/go/issues/23044 ballast := make([]byte, *ballastBytes) defer runtime.KeepAlive(ballast) @@ -89,7 +93,7 @@ func exportCmd() *cli.Command { // find possible environments if *recursive { // get absolute path to Environment - envs, err := tanka.FindEnvsFromPaths(args[1:], tanka.FindOpts{Selector: opts.Selector, Parallelism: opts.Parallelism, JsonnetOpts: opts.Opts.JsonnetOpts}) + envs, err := tanka.FindEnvsFromPaths(ctx, args[1:], tanka.FindOpts{Selector: opts.Selector, Parallelism: opts.Parallelism, JsonnetOpts: opts.Opts.JsonnetOpts}) if err != nil { return err } @@ -106,7 +110,7 @@ func exportCmd() *cli.Command { } // validate environment - env, err := tanka.Peek(args[1], opts.Opts) + env, err := tanka.Peek(ctx, args[1], opts.Opts) if err != nil { switch err.(type) { case tanka.ErrMultipleEnvs: @@ -121,7 +125,7 @@ func exportCmd() *cli.Command { } // export them - return tanka.ExportEnvironments(exportEnvs, args[0], &opts) + return tanka.ExportEnvironments(ctx, exportEnvs, args[0], &opts) } return cmd } diff --git a/cmd/tk/fmt.go b/cmd/tk/fmt.go index f73b67c06..12bbd2de8 100644 --- a/cmd/tk/fmt.go +++ b/cmd/tk/fmt.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io" "os" @@ -15,7 +16,7 @@ import ( // ArgStdin is the "magic" argument for reading from stdin const ArgStdin = "-" -func fmtCmd() *cli.Command { +func fmtCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "fmt ", Short: "format Jsonnet code", @@ -31,6 +32,8 @@ func fmtCmd() *cli.Command { verbose := cmd.Flags().BoolP("verbose", "v", false, "print each checked file") cmd.Run = func(_ *cli.Command, args []string) error { + _, span := tracer.Start(ctx, "fmtCmd") + defer span.End() if len(args) == 1 && args[0] == ArgStdin { return fmtStdin(*test) } diff --git a/cmd/tk/init.go b/cmd/tk/init.go index 8b49be2d1..95b2dafa0 100644 --- a/cmd/tk/init.go +++ b/cmd/tk/init.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "os" @@ -15,7 +16,7 @@ import ( const defaultK8sVersion = "1.29" // initCmd creates a new application -func initCmd() *cli.Command { +func initCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "init", Short: "Create the directory structure", @@ -27,6 +28,8 @@ func initCmd() *cli.Command { inline := cmd.Flags().BoolP("inline", "i", false, "create an inline environment") cmd.Run = func(_ *cli.Command, _ []string) error { + _, span := tracer.Start(ctx, "initCmd") + defer span.End() failed := false files, err := os.ReadDir(".") diff --git a/cmd/tk/jsonnet.go b/cmd/tk/jsonnet.go index c6dfbcf4e..7a1ee434c 100644 --- a/cmd/tk/jsonnet.go +++ b/cmd/tk/jsonnet.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "github.com/go-clix/cli" @@ -8,11 +9,11 @@ import ( "github.com/grafana/tanka/pkg/tanka" ) -func evalCmd() *cli.Command { +func evalCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Short: "evaluate the jsonnet to json", Use: "eval ", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), } var jsonnetImplementation string @@ -29,7 +30,7 @@ func evalCmd() *cli.Command { if *evalPattern != "" { jsonnetOpts.EvalScript = tanka.PatternEvalScript(*evalPattern) } - raw, err := tanka.Eval(args[0], jsonnetOpts) + raw, err := tanka.Eval(ctx, args[0], jsonnetOpts) if raw == nil && err != nil { return err diff --git a/cmd/tk/lint.go b/cmd/tk/lint.go index bad3679ba..655b255ad 100644 --- a/cmd/tk/lint.go +++ b/cmd/tk/lint.go @@ -1,6 +1,8 @@ package main import ( + "context" + "github.com/go-clix/cli" "github.com/gobwas/glob" "github.com/posener/complete" @@ -8,7 +10,7 @@ import ( "github.com/grafana/tanka/pkg/jsonnet" ) -func lintCmd() *cli.Command { +func lintCmd(_ context.Context) *cli.Command { cmd := &cli.Command{ Use: "lint ", Short: "lint Jsonnet code", diff --git a/cmd/tk/main.go b/cmd/tk/main.go index b988b4b53..b5e745289 100644 --- a/cmd/tk/main.go +++ b/cmd/tk/main.go @@ -1,19 +1,26 @@ package main import ( + "context" "fmt" "os" "strings" "github.com/go-clix/cli" "github.com/rs/zerolog" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.34.0" "golang.org/x/term" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/tanka" ) var interactive = term.IsTerminal(int(os.Stdout.Fd())) +var tracer = telemetry.Tracer("tanka") + func main() { rootCmd := &cli.Command{ Use: "tk", @@ -21,34 +28,46 @@ func main() { Version: tanka.CurrentVersion, } + ctx := context.Background() + res := resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName("tanka"), + ) + shutdownOtel, err := telemetry.Setup(ctx, res) + if err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(1) + } + ctx = otel.GetTextMapPropagator().Extract(ctx, telemetry.LoadEnvironmentCarrier()) + // set default logging level early; not all commands parse --log-level zerolog.SetGlobalLevel(zerolog.InfoLevel) // workflow commands addCommandsWithLogLevelOption( rootCmd, - applyCmd(), - showCmd(), - diffCmd(), - pruneCmd(), - deleteCmd(), + applyCmd(ctx), + showCmd(ctx), + diffCmd(ctx), + pruneCmd(ctx), + deleteCmd(ctx), ) addCommandsWithLogLevelOption( rootCmd, - envCmd(), - statusCmd(), - exportCmd(), + envCmd(ctx), + statusCmd(ctx), + exportCmd(ctx), ) // jsonnet commands addCommandsWithLogLevelOption( rootCmd, - fmtCmd(), - lintCmd(), - evalCmd(), - initCmd(), - toolCmd(), + fmtCmd(ctx), + lintCmd(ctx), + evalCmd(ctx), + initCmd(ctx), + toolCmd(ctx), ) // external commands prefixed with "tk-" @@ -59,9 +78,15 @@ func main() { // Run! if err := rootCmd.Execute(); err != nil { + if err := shutdownOtel(context.Background()); err != nil { + fmt.Fprintln(os.Stderr, "OTEL shutdown error:", err) + } fmt.Fprintln(os.Stderr, "Error:", err) os.Exit(1) } + if err := shutdownOtel(context.Background()); err != nil { + fmt.Fprintln(os.Stderr, "OTEL shutdown error:", err) + } } func addCommandsWithLogLevelOption(rootCmd *cli.Command, cmds ...*cli.Command) { diff --git a/cmd/tk/status.go b/cmd/tk/status.go index 8d76c5676..2698e67d4 100644 --- a/cmd/tk/status.go +++ b/cmd/tk/status.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "os" "sort" @@ -13,18 +14,18 @@ import ( "github.com/grafana/tanka/pkg/tanka" ) -func statusCmd() *cli.Command { +func statusCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "status ", Short: "display an overview of the environment, including contents and metadata.", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), } vars := workflowFlags(cmd.Flags()) getJsonnetOpts := jsonnetFlags(cmd.Flags()) cmd.Run = func(_ *cli.Command, args []string) error { - status, err := tanka.Status(args[0], tanka.Opts{ + status, err := tanka.Status(ctx, args[0], tanka.Opts{ JsonnetImplementation: vars.jsonnetImplementation, JsonnetOpts: getJsonnetOpts(), Name: vars.name, diff --git a/cmd/tk/tool.go b/cmd/tk/tool.go index 7f25fcabb..69cf336a2 100644 --- a/cmd/tk/tool.go +++ b/cmd/tk/tool.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "encoding/json" "fmt" "os" @@ -16,7 +17,7 @@ import ( "github.com/grafana/tanka/pkg/jsonnet/jpath" ) -func toolCmd() *cli.Command { +func toolCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Short: "handy utilities for working with jsonnet", Use: "tool [command]", @@ -25,20 +26,20 @@ func toolCmd() *cli.Command { addCommandsWithLogLevelOption( cmd, - jpathCmd(), - importsCmd(), - importersCmd(), - importersCountCmd(), - chartsCmd(), + jpathCmd(ctx), + importsCmd(ctx), + importersCmd(ctx), + importersCountCmd(ctx), + chartsCmd(ctx), ) return cmd } -func jpathCmd() *cli.Command { +func jpathCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Short: "export JSONNET_PATH for use with other jsonnet tools", Use: "jpath ", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), } debug := cmd.Flags().BoolP("debug", "d", false, "show debug info") @@ -73,16 +74,19 @@ func jpathCmd() *cli.Command { return cmd } -func importsCmd() *cli.Command { +func importsCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "imports ", Short: "list all transitive imports of an environment", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), } check := cmd.Flags().StringP("check", "c", "", "git commit hash to check against") cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "importsCmd") + defer span.End() + var modFiles []string if *check != "" { var err error @@ -97,7 +101,7 @@ func importsCmd() *cli.Command { return fmt.Errorf("loading environment: %s", err) } - deps, err := jsonnet.TransitiveImports(path) + deps, err := jsonnet.TransitiveImports(ctx, path) if err != nil { return fmt.Errorf("resolving imports: %s", err) } @@ -136,7 +140,7 @@ func importsCmd() *cli.Command { return cmd } -func importersCmd() *cli.Command { +func importersCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "importers ", Short: "list all environments that either directly or transitively import the given files", @@ -158,6 +162,9 @@ if the file is not a vendored (located at /vendor/) or a lib file (loca root := cmd.Flags().String("root", ".", "root directory to search for environments") cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "importersCmd") + defer span.End() + root, err := filepath.Abs(*root) if err != nil { return fmt.Errorf("resolving root: %w", err) @@ -172,7 +179,7 @@ if the file is not a vendored (located at /vendor/) or a lib file (loca } } - envs, err := jsonnet.FindImporterForFiles(root, args) + envs, err := jsonnet.FindImporterForFiles(ctx, root, args) if err != nil { return fmt.Errorf("resolving imports: %s", err) } @@ -185,7 +192,7 @@ if the file is not a vendored (located at /vendor/) or a lib file (loca return cmd } -func importersCountCmd() *cli.Command { +func importersCountCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "importers-count ", Short: "for each file in the given directory, list the number of environments that import it", @@ -209,6 +216,8 @@ if the file is not a vendored (located at /vendor/) or a lib file (loca filenameRegex := cmd.Flags().String("filename-regex", "", "only count files that match the given regex. Matches only jsonnet files by default") cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "importersCountCmd") + defer span.End() dir := args[0] root, err := filepath.Abs(*root) @@ -216,7 +225,7 @@ if the file is not a vendored (located at /vendor/) or a lib file (loca return fmt.Errorf("resolving root: %w", err) } - result, err := jsonnet.CountImporters(root, dir, *recursive, *filenameRegex) + result, err := jsonnet.CountImporters(ctx, root, dir, *recursive, *filenameRegex) if err != nil { return fmt.Errorf("resolving imports: %s", err) } diff --git a/cmd/tk/toolCharts.go b/cmd/tk/toolCharts.go index 57047fb23..440beaa9b 100644 --- a/cmd/tk/toolCharts.go +++ b/cmd/tk/toolCharts.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "fmt" "os" @@ -13,7 +14,7 @@ import ( const repoConfigFlagUsage = "specify a local helm repository config file to use instead of the repositories in the chartfile.yaml. For use with private repositories" -func chartsCmd() *cli.Command { +func chartsCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "charts", Short: "Declarative vendoring of Helm Charts", @@ -22,18 +23,18 @@ func chartsCmd() *cli.Command { addCommandsWithLogLevelOption( cmd, - chartsInitCmd(), - chartsAddCmd(), - chartsAddRepoCmd(), - chartsVendorCmd(), - chartsConfigCmd(), - chartsVersionCheckCmd(), + chartsInitCmd(ctx), + chartsAddCmd(ctx), + chartsAddRepoCmd(ctx), + chartsVendorCmd(ctx), + chartsConfigCmd(ctx), + chartsVersionCheckCmd(ctx), ) return cmd } -func chartsVendorCmd() *cli.Command { +func chartsVendorCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "vendor", Short: "Download Charts to a local folder", @@ -42,6 +43,8 @@ func chartsVendorCmd() *cli.Command { repoConfigPath := cmd.Flags().String("repository-config", "", repoConfigFlagUsage) cmd.Run = func(_ *cli.Command, _ []string) error { + _, span := tracer.Start(ctx, "chartsVendorCmd") + defer span.End() c, err := loadChartfile() if err != nil { return err @@ -53,7 +56,7 @@ func chartsVendorCmd() *cli.Command { return cmd } -func chartsAddCmd() *cli.Command { +func chartsAddCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "add [chart@version] [...]", Short: "Adds Charts to the chartfile", @@ -61,6 +64,8 @@ func chartsAddCmd() *cli.Command { repoConfigPath := cmd.Flags().String("repository-config", "", repoConfigFlagUsage) cmd.Run = func(_ *cli.Command, args []string) error { + _, span := tracer.Start(ctx, "chartsVendorCmd") + defer span.End() c, err := loadChartfile() if err != nil { return err @@ -72,7 +77,7 @@ func chartsAddCmd() *cli.Command { return cmd } -func chartsAddRepoCmd() *cli.Command { +func chartsAddRepoCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "add-repo [NAME] [URL]", Short: "Adds a repository to the chartfile", @@ -80,6 +85,8 @@ func chartsAddRepoCmd() *cli.Command { } cmd.Run = func(_ *cli.Command, args []string) error { + _, span := tracer.Start(ctx, "chartsVendorCmd") + defer span.End() c, err := loadChartfile() if err != nil { return err @@ -94,13 +101,15 @@ func chartsAddRepoCmd() *cli.Command { return cmd } -func chartsConfigCmd() *cli.Command { +func chartsConfigCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "config", Short: "Displays the current manifest", } cmd.Run = func(_ *cli.Command, _ []string) error { + _, span := tracer.Start(ctx, "chartsVendorCmd") + defer span.End() c, err := loadChartfile() if err != nil { return err @@ -119,13 +128,15 @@ func chartsConfigCmd() *cli.Command { return cmd } -func chartsInitCmd() *cli.Command { +func chartsInitCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "init", Short: "Create a new Chartfile", } cmd.Run = func(_ *cli.Command, _ []string) error { + _, span := tracer.Start(ctx, "chartsVendorCmd") + defer span.End() wd, err := os.Getwd() if err != nil { return err @@ -147,7 +158,7 @@ func chartsInitCmd() *cli.Command { return cmd } -func chartsVersionCheckCmd() *cli.Command { +func chartsVersionCheckCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "version-check", Short: "Check required charts for updated versions", @@ -156,6 +167,8 @@ func chartsVersionCheckCmd() *cli.Command { prettyPrint := cmd.Flags().Bool("pretty-print", false, "pretty print json output with indents") cmd.Run = func(_ *cli.Command, _ []string) error { + _, span := tracer.Start(ctx, "chartsVendorCmd") + defer span.End() c, err := loadChartfile() if err != nil { return err diff --git a/cmd/tk/workflow.go b/cmd/tk/workflow.go index 64fbdb2b9..1af871b83 100644 --- a/cmd/tk/workflow.go +++ b/cmd/tk/workflow.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "os" @@ -69,11 +70,11 @@ func validateAutoApprove(autoApproveDeprecated bool, autoApproveString string) ( return result, nil } -func applyCmd() *cli.Command { +func applyCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "apply ", Short: "apply the configuration to the cluster", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), Predictors: complete.Flags{ "color": colorValues, "diff-strategy": cli.PredictSet("native", "subset", "validate", "server", "none"), @@ -96,6 +97,8 @@ func applyCmd() *cli.Command { getJsonnetOpts := jsonnetFlags(cmd.Flags()) cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "applyCmd") + defer span.End() err := validateDryRun(opts.DryRun) if err != nil { return err @@ -116,16 +119,16 @@ func applyCmd() *cli.Command { opts.Name = vars.name opts.JsonnetImplementation = vars.jsonnetImplementation - return tanka.Apply(args[0], opts) + return tanka.Apply(ctx, args[0], opts) } return cmd } -func pruneCmd() *cli.Command { +func pruneCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "prune ", Short: "delete resources removed from Jsonnet", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), Predictors: complete.Flags{ "color": colorValues, }, @@ -142,6 +145,8 @@ func pruneCmd() *cli.Command { getJsonnetOpts := jsonnetFlags(cmd.Flags()) cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "pruneCmd") + defer span.End() err := validateDryRun(opts.DryRun) if err != nil { return err @@ -155,17 +160,17 @@ func pruneCmd() *cli.Command { opts.JsonnetOpts = getJsonnetOpts() - return tanka.Prune(args[0], opts) + return tanka.Prune(ctx, args[0], opts) } return cmd } -func deleteCmd() *cli.Command { +func deleteCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "delete ", Short: "delete the environment from cluster", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), Predictors: complete.Flags{ "color": colorValues, }, @@ -183,6 +188,8 @@ func deleteCmd() *cli.Command { getJsonnetOpts := jsonnetFlags(cmd.Flags()) cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "deleteCmd") + defer span.End() err := validateDryRun(opts.DryRun) if err != nil { return err @@ -203,16 +210,16 @@ func deleteCmd() *cli.Command { opts.Name = vars.name opts.JsonnetImplementation = vars.jsonnetImplementation - return tanka.Delete(args[0], opts) + return tanka.Delete(ctx, args[0], opts) } return cmd } -func diffCmd() *cli.Command { +func diffCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "diff ", Short: "differences between the configuration and the cluster", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), Predictors: complete.Flags{ "color": colorValues, "diff-strategy": cli.PredictSet("native", "subset", "validate", "server"), @@ -230,6 +237,8 @@ func diffCmd() *cli.Command { getJsonnetOpts := jsonnetFlags(cmd.Flags()) cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "diffCmd") + defer span.End() if err := setForceColor(&opts.DiffBaseOpts); err != nil { return err } @@ -242,7 +251,7 @@ func diffCmd() *cli.Command { opts.Name = vars.name opts.JsonnetImplementation = vars.jsonnetImplementation - changes, err := tanka.Diff(args[0], opts) + changes, err := tanka.Diff(ctx, args[0], opts) if err != nil { return err } @@ -261,6 +270,7 @@ func diffCmd() *cli.Command { if opts.ExitZero { exitStatusDiff = ExitStatusClean } + span.End() os.Exit(exitStatusDiff) return nil } @@ -268,11 +278,11 @@ func diffCmd() *cli.Command { return cmd } -func showCmd() *cli.Command { +func showCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "show ", Short: "jsonnet as yaml", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), } allowRedirectFlag := cmd.Flags().Bool("dangerous-allow-redirect", false, "allow redirecting output to a file or a pipe.") @@ -281,6 +291,8 @@ func showCmd() *cli.Command { getJsonnetOpts := jsonnetFlags(cmd.Flags()) cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "showCmd") + defer span.End() allowRedirectEnv := os.Getenv("TANKA_DANGEROUS_ALLOW_REDIRECT") == "true" allowRedirect := allowRedirectEnv || *allowRedirectFlag @@ -300,7 +312,7 @@ to bypass this check.`) return err } - pretty, err := tanka.Show(args[0], tanka.Opts{ + pretty, err := tanka.Show(ctx, args[0], tanka.Opts{ JsonnetOpts: getJsonnetOpts(), Filters: filters, Name: vars.name, diff --git a/docs/astro.config.ts b/docs/astro.config.ts index a737c0884..dc730179d 100644 --- a/docs/astro.config.ts +++ b/docs/astro.config.ts @@ -104,6 +104,10 @@ export default defineConfig({ label: 'Server-Side Apply', link: '/server-side-apply', }, + { + label: 'Telemetry', + link: '/telemetry', + }, ], }, { diff --git a/docs/src/content/docs/telemetry.md b/docs/src/content/docs/telemetry.md new file mode 100644 index 000000000..b6f506909 --- /dev/null +++ b/docs/src/content/docs/telemetry.md @@ -0,0 +1,12 @@ +--- +title: Telemetry +--- + +Tanka supports sending OpenTelemetry traces to a `http/protobuf` endpoint. +To use this, export the following environment variable: + +``` +export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 +``` + +Note that we try to keep traces to the critical paths around the `export` command since these are usually the areas where performance is most important in automated workflows. diff --git a/go.mod b/go.mod index 3dadb6a63..729ff6d15 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,10 @@ require ( github.com/stretchr/objx v0.5.2 github.com/stretchr/testify v1.11.0 github.com/thoas/go-funk v0.9.3 + go.opentelemetry.io/otel v1.37.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 + go.opentelemetry.io/otel/sdk v1.37.0 + go.opentelemetry.io/otel/trace v1.37.0 golang.org/x/term v0.34.0 golang.org/x/text v0.28.0 gopkg.in/yaml.v2 v2.4.0 @@ -33,9 +37,12 @@ require ( dario.cat/mergo v1.0.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/xstrings v1.5.0 // indirect @@ -46,9 +53,18 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.7.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/crypto v0.36.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/sys v0.35.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/grpc v1.73.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect k8s.io/klog/v2 v2.130.1 // indirect ) diff --git a/go.sum b/go.sum index 18f658304..8b9e63b06 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+ github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -20,17 +22,24 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-clix/cli v0.2.0 h1:rqpcyS/cvshOhXkwii0V+7nWetDVC8cp4pKI7JiCIS8= github.com/go-clix/cli v0.2.0/go.mod h1:yWI9abpv187r47lDjz8Z9TWev93aUTWaW2seSb5JmPQ= -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/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/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/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-jsonnet v0.21.0 h1:43Bk3K4zMRP/aAZm9Po2uSEjY6ALCkYUVIcz9HLGMvA= github.com/google/go-jsonnet v0.21.0/go.mod h1:tCGAu8cpUpEZcdGMmdOu37nh8bGgqubhI5v2iSk3KJQ= 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/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= 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= @@ -39,11 +48,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -85,12 +91,34 @@ github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQ github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw= github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -100,6 +128,14 @@ golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/go.work.sum b/go.work.sum index fdea6615d..80463a807 100644 --- a/go.work.sum +++ b/go.work.sum @@ -3,18 +3,26 @@ cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8= cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= +codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw= +codeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= +git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94= github.com/99designs/gqlgen v0.17.69 h1:9lGNxnxEnrlTkDn8g6IzcRi9Io3XyMLscrHWDKgdXWQ= github.com/99designs/gqlgen v0.17.69/go.mod h1:fvCiqQAu2VLhKXez2xFvLmE47QgAPf/KTPN5XQ4rsHQ= github.com/99designs/gqlgen v0.17.70 h1:xgLIgQuG+Q2L/AE9cW595CT7xCWCe/bpPIFGSfsGSGs= github.com/99designs/gqlgen v0.17.70/go.mod h1:fvCiqQAu2VLhKXez2xFvLmE47QgAPf/KTPN5XQ4rsHQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= @@ -22,6 +30,7 @@ github.com/PuerkitoBio/goquery v1.9.3/go.mod h1:1ndLHPdTz+DyQPICCWYlYQMPl0oXZj0G github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM= github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= @@ -34,6 +43,7 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -47,6 +57,7 @@ github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1Ig github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -65,6 +76,9 @@ github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQL github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= @@ -75,10 +89,14 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4 github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198/go.mod h1:DTh/Y2+NbnOVVoypCCQrovMPDKUGp4yZpSbWg5D0XIM= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -93,11 +111,13 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM= github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= github.com/logrusorgru/aurora/v4 v4.0.0/go.mod h1:lP0iIa2nrnT/qoFXcOZSrZQpJ1o6n2CUf/hyHi2Q4ZQ= +github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matryer/moq v0.3.3/go.mod h1:RJ75ZZZD71hejp39j4crZLsEDszGk6iH4v4YsWFKH4s= github.com/matryer/moq v0.3.4/go.mod h1:wqm9QObyoMuUtH81zFfs3EK6mXEcByy+TjvSROOXJ2U= @@ -119,9 +139,11 @@ github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKi github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= 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= @@ -143,6 +165,7 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240625175500-6d45f283c7ab h1:2eq3ZKzQLi1gFNwUw+4n+gsu9klL34RHm93MHitSLYo= @@ -154,14 +177,11 @@ go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0/go.mod h1:51 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= -go.opentelemetry.io/otel/log v0.13.0 h1:yoxRoIZcohB6Xf0lNv9QIyCzQvrtGZklVbdCoyb7dls= -go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E= go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= -go.opentelemetry.io/otel/sdk/log v0.13.0 h1:I3CGUszjM926OphK8ZdzF+kLqFvfRY/IIoFq/TjwfaQ= -go.opentelemetry.io/otel/sdk/log v0.13.0/go.mod h1:lOrQyCCXmpZdN7NchXb6DOZZa1N5G1R2tm5GMMTpDBw= +go.opentelemetry.io/otel/sdk/log/logtest v0.13.0/go.mod h1:QOGiAJHl+fob8Nu85ifXfuQYmJTFAvcrxL6w5/tu168= go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= @@ -174,8 +194,9 @@ golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= @@ -185,6 +206,7 @@ golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= @@ -201,6 +223,7 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -211,16 +234,20 @@ golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= @@ -237,17 +264,25 @@ golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas= google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d h1:Aqf0fiIdUQEj0Gn9mKFFXoQfTTEaNopWpfVyYADxiSg= google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Od4k8V1LQSizPRUK4OzZ7TBE/20k+jPczUDAEyvn69Y= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk= google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= 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.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= @@ -262,5 +297,6 @@ k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4Va k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/internal/telemetry/attributes.go b/internal/telemetry/attributes.go new file mode 100644 index 000000000..0b2d63015 --- /dev/null +++ b/internal/telemetry/attributes.go @@ -0,0 +1,26 @@ +package telemetry + +import ( + "fmt" + + "github.com/grafana/tanka/pkg/spec/v1alpha1" + "go.opentelemetry.io/otel/attribute" +) + +func AttrPath(v string) attribute.KeyValue { + return attribute.String("tanka.path", v) +} + +func AttrLoader(v string) attribute.KeyValue { + return attribute.String("tanka.loader", v) +} + +func AttrNumEnvs(v int) attribute.KeyValue { + return attribute.Int("tanka.envs.num", v) +} + +func AttrEnv(v *v1alpha1.Environment) []attribute.KeyValue { + return []attribute.KeyValue{ + attribute.String("tanka.env.id", fmt.Sprintf("%s@%s", v.Metadata.Name, v.Spec.APIServer)), + } +} diff --git a/internal/telemetry/otel.go b/internal/telemetry/otel.go new file mode 100644 index 000000000..752889201 --- /dev/null +++ b/internal/telemetry/otel.go @@ -0,0 +1,123 @@ +package telemetry + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" +) + +func generateTracerProvider(ctx context.Context, res *resource.Resource) (*sdktrace.TracerProvider, error) { + if !hasOTELConfig(os.Environ()) { + return nil, nil + } + traceExporter, err := otlptracehttp.New(ctx) + if err != nil { + return nil, fmt.Errorf("failed to set up trace exporter: %w", err) + } + tracerProvider := sdktrace.NewTracerProvider( + sdktrace.WithResource(res), + sdktrace.WithBatcher(traceExporter, + sdktrace.WithBatchTimeout(time.Second*5)), + ) + return tracerProvider, nil +} + +func hasOTELConfig(env []string) bool { + for _, envName := range env { + if strings.HasPrefix(envName, "OTEL_") { + return true + } + } + return false +} + +func generatePropagator() propagation.TextMapPropagator { + return propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) +} + +func Setup(ctx context.Context, res *resource.Resource) (func(context.Context) error, error) { + var shutdownFuncs []func(context.Context) error + var err error + + shutdown := func(ctx context.Context) error { + var err error + for _, fn := range shutdownFuncs { + err = errors.Join(err, fn(ctx)) + } + shutdownFuncs = nil + return err + } + + handleError := func(e error) { + err = errors.Join(e, shutdown(ctx)) + } + + finalRes, err := resource.Merge(resource.Default(), res) + if err != nil { + return nil, err + } + + prop := generatePropagator() + otel.SetTextMapPropagator(prop) + + tracerProvider, err := generateTracerProvider(ctx, finalRes) + if err != nil { + handleError(err) + return shutdown, err + } + if tracerProvider != nil { + shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) + otel.SetTracerProvider(tracerProvider) + } + + return shutdown, err +} + +func Tracer(name string) trace.Tracer { + return otel.Tracer(name) +} + +func FailSpanWithError(span trace.Span, err error) { + if err == nil { + return + } + span.RecordError(err) + span.SetStatus(codes.Error, "") +} + +func SucceedSpan(span trace.Span) { + span.SetStatus(codes.Ok, "") +} + +func InjectIntoEnv(ctx context.Context, env []string) []string { + carrier := make(propagation.MapCarrier) + prop := otel.GetTextMapPropagator() + prop.Inject(ctx, &carrier) + env = append(env, fmt.Sprintf("BAGGAGE=%s", carrier.Get("baggage"))) + env = append(env, fmt.Sprintf("TRACEPARENT=%s", carrier.Get("traceparent"))) + env = append(env, fmt.Sprintf("TRACESTATE=%s", carrier.Get("tracestate"))) + return env +} + +func LoadEnvironmentCarrier() propagation.TextMapCarrier { + carrier := make(propagation.MapCarrier) + carrier.Set("baggage", os.Getenv("BAGGAGE")) + carrier.Set("traceparent", os.Getenv("TRACEPARENT")) + carrier.Set("tracestate", os.Getenv("TRACESTATE")) + fmt.Println(carrier) + return carrier +} diff --git a/pkg/jsonnet/eval.go b/pkg/jsonnet/eval.go index 16901982f..529014800 100644 --- a/pkg/jsonnet/eval.go +++ b/pkg/jsonnet/eval.go @@ -1,6 +1,7 @@ package jsonnet import ( + "context" "os" "regexp" "time" @@ -79,7 +80,7 @@ func (o Opts) Clone() Opts { // EvaluateFile evaluates the Jsonnet code in the given file and returns the // result in JSON form. It disregards opts.ImportPaths in favor of automatically // resolving these according to the specified file. -func EvaluateFile(impl types.JsonnetImplementation, jsonnetFile string, opts Opts) (string, error) { +func EvaluateFile(_ context.Context, impl types.JsonnetImplementation, jsonnetFile string, opts Opts) (string, error) { evalFunc := func(evaluator types.JsonnetEvaluator) (string, error) { return evaluator.EvaluateFile(jsonnetFile) } @@ -93,7 +94,7 @@ func EvaluateFile(impl types.JsonnetImplementation, jsonnetFile string, opts Opt // Evaluate renders the given jsonnet into a string // If cache options are given, a hash from the data will be computed and // the resulting string will be cached for future retrieval -func Evaluate(path string, impl types.JsonnetImplementation, data string, opts Opts) (string, error) { +func Evaluate(_ context.Context, path string, impl types.JsonnetImplementation, data string, opts Opts) (string, error) { evalFunc := func(evaluator types.JsonnetEvaluator) (string, error) { return evaluator.EvaluateAnonymousSnippet(data) } diff --git a/pkg/jsonnet/eval_test.go b/pkg/jsonnet/eval_test.go index a1051502a..345888ce0 100644 --- a/pkg/jsonnet/eval_test.go +++ b/pkg/jsonnet/eval_test.go @@ -56,14 +56,14 @@ const thisFileResult = `{ // To be consistent with the jsonnet executable, // when evaluating a file, `std.thisFile` should point to the given path func TestEvaluateFile(t *testing.T) { - result, err := EvaluateFile(jsonnetImpl, "testdata/thisFile/main.jsonnet", Opts{}) + result, err := EvaluateFile(t.Context(), jsonnetImpl, "testdata/thisFile/main.jsonnet", Opts{}) assert.NoError(t, err) assert.Equal(t, thisFileResult, result) } func TestEvaluateFileWithInvalidBinary(t *testing.T) { binaryImpl := &binary.JsonnetBinaryImplementation{BinPath: "this-file-doesnt-exist"} - result, err := EvaluateFile(binaryImpl, "testdata/thisFile/main.jsonnet", Opts{}) + result, err := EvaluateFile(t.Context(), binaryImpl, "testdata/thisFile/main.jsonnet", Opts{}) assert.Equal(t, result, "") assert.ErrorIs(t, err, exec.ErrNotFound) } @@ -71,13 +71,13 @@ func TestEvaluateFileWithInvalidBinary(t *testing.T) { // This test requires jsonnet to be installed and available in the PATH func TestEvaluateFileWithJsonnetBinary(t *testing.T) { binaryImpl := &binary.JsonnetBinaryImplementation{BinPath: "jsonnet"} - result, err := EvaluateFile(binaryImpl, "testdata/thisFile/main.jsonnet", Opts{}) + result, err := EvaluateFile(t.Context(), binaryImpl, "testdata/thisFile/main.jsonnet", Opts{}) assert.NoError(t, err) assert.Equal(t, thisFileResult, result) } func TestEvaluateFileDoesntExist(t *testing.T) { - result, err := EvaluateFile(jsonnetImpl, "testdata/doesnt-exist/main.jsonnet", Opts{}) + result, err := EvaluateFile(t.Context(), jsonnetImpl, "testdata/doesnt-exist/main.jsonnet", Opts{}) assert.EqualError(t, err, "open testdata/doesnt-exist/main.jsonnet: no such file or directory") assert.Equal(t, "", result) } @@ -89,10 +89,10 @@ func TestEvaluateFileWithCaching(t *testing.T) { cachePath := filepath.Join(tmp, "cache") // Should be created during caching // Evaluate two files - result, err := EvaluateFile(jsonnetImpl, "testdata/thisFile/main.jsonnet", Opts{CachePath: cachePath}) + result, err := EvaluateFile(t.Context(), jsonnetImpl, "testdata/thisFile/main.jsonnet", Opts{CachePath: cachePath}) assert.NoError(t, err) assert.Equal(t, thisFileResult, result) - result, err = EvaluateFile(jsonnetImpl, "testdata/importTree/main.jsonnet", Opts{CachePath: cachePath}) + result, err = EvaluateFile(t.Context(), jsonnetImpl, "testdata/importTree/main.jsonnet", Opts{CachePath: cachePath}) assert.NoError(t, err) assert.Equal(t, importTreeResult, result) @@ -102,10 +102,10 @@ func TestEvaluateFileWithCaching(t *testing.T) { assert.Len(t, readCache, 2) // Evaluate two files again, same result - result, err = EvaluateFile(jsonnetImpl, "testdata/thisFile/main.jsonnet", Opts{CachePath: cachePath}) + result, err = EvaluateFile(t.Context(), jsonnetImpl, "testdata/thisFile/main.jsonnet", Opts{CachePath: cachePath}) assert.NoError(t, err) assert.Equal(t, thisFileResult, result) - result, err = EvaluateFile(jsonnetImpl, "testdata/importTree/main.jsonnet", Opts{CachePath: cachePath}) + result, err = EvaluateFile(t.Context(), jsonnetImpl, "testdata/importTree/main.jsonnet", Opts{CachePath: cachePath}) assert.NoError(t, err) assert.Equal(t, importTreeResult, result) @@ -115,10 +115,10 @@ func TestEvaluateFileWithCaching(t *testing.T) { } // Evaluate two files again, modified cache is returned instead of the actual result - result, err = EvaluateFile(jsonnetImpl, "testdata/thisFile/main.jsonnet", Opts{CachePath: cachePath}) + result, err = EvaluateFile(t.Context(), jsonnetImpl, "testdata/thisFile/main.jsonnet", Opts{CachePath: cachePath}) assert.NoError(t, err) assert.Equal(t, "BYfdlr1ZOVwiOfbd89JYTcK-eRQh05bi8ky3k1vVW5o=.json", result) - result, err = EvaluateFile(jsonnetImpl, "testdata/importTree/main.jsonnet", Opts{CachePath: cachePath}) + result, err = EvaluateFile(t.Context(), jsonnetImpl, "testdata/importTree/main.jsonnet", Opts{CachePath: cachePath}) assert.NoError(t, err) assert.Equal(t, "R_3hy-dRfOwXN-fezQ50ZF4dnrFcBcbQ9LztR_XWzJA=.json", result) } diff --git a/pkg/jsonnet/find_importers.go b/pkg/jsonnet/find_importers.go index 492bc512b..b682e3b1c 100644 --- a/pkg/jsonnet/find_importers.go +++ b/pkg/jsonnet/find_importers.go @@ -1,6 +1,7 @@ package jsonnet import ( + "context" "fmt" "os" "path/filepath" @@ -8,7 +9,9 @@ import ( "sort" "strings" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/jsonnet/jpath" + "go.opentelemetry.io/otel/attribute" ) var ( @@ -27,9 +30,14 @@ type cachedJsonnetFile struct { // FindImporterForFiles finds the entrypoints (main.jsonnet files) that import the given files. // It looks through imports transitively, so if a file is imported through a chain, it will still be reported. // If the given file is a main.jsonnet file, it will be returned as well. -func FindImporterForFiles(root string, files []string) ([]string, error) { +func FindImporterForFiles(ctx context.Context, root string, files []string) ([]string, error) { + _, span := tracer.Start(ctx, "jsonnet.FindImporterForFiles") + defer span.End() + + span.SetAttributes(attribute.StringSlice("tanka.files", files)) transitiveImporters, err := FindTransitiveImportersForFile(root, files) if err != nil { + telemetry.FailSpanWithError(span, err) return nil, err } @@ -97,10 +105,14 @@ func FindTransitiveImportersForFile(root string, files []string) ([]string, erro } // CountImporters lists all the files in the given directory and for each file counts the number of environments that import it. -func CountImporters(root string, dir string, recursive bool, filenameRegexStr string) (string, error) { +func CountImporters(ctx context.Context, root string, dir string, recursive bool, filenameRegexStr string) (string, error) { + ctx, span := tracer.Start(ctx, "jsonnet.CountImporters") + defer span.End() root, err := filepath.Abs(root) if err != nil { - return "", fmt.Errorf("resolving root: %w", err) + err = fmt.Errorf("resolving root: %w", err) + telemetry.FailSpanWithError(span, err) + return "", err } if filenameRegexStr == "" { @@ -108,7 +120,9 @@ func CountImporters(root string, dir string, recursive bool, filenameRegexStr st } filenameRegexp, err := regexp.Compile(filenameRegexStr) if err != nil { - return "", fmt.Errorf("compiling filename regex: %w", err) + err = fmt.Errorf("compiling filename regex: %w", err) + telemetry.FailSpanWithError(span, err) + return "", err } var files []string err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { @@ -141,14 +155,18 @@ func CountImporters(root string, dir string, recursive bool, filenameRegexStr st return nil }) if err != nil { - return "", fmt.Errorf("walking directory: %w", err) + err = fmt.Errorf("walking directory: %w", err) + telemetry.FailSpanWithError(span, err) + return "", err } importers := map[string]int{} for _, file := range files { - importersList, err := FindImporterForFiles(root, []string{file}) + importersList, err := FindImporterForFiles(ctx, root, []string{file}) if err != nil { - return "", fmt.Errorf("resolving imports: %w", err) + err = fmt.Errorf("resolving imports: %w", err) + telemetry.FailSpanWithError(span, err) + return "", err } importers[file] = len(importersList) } diff --git a/pkg/jsonnet/find_importers_test.go b/pkg/jsonnet/find_importers_test.go index 821562089..750395b9d 100644 --- a/pkg/jsonnet/find_importers_test.go +++ b/pkg/jsonnet/find_importers_test.go @@ -21,7 +21,7 @@ type findImportersTestCase struct { } func (tc findImportersTestCase) run(t testing.TB) { - importers, err := FindImporterForFiles("testdata/findImporters", tc.files) + importers, err := FindImporterForFiles(t.Context(), "testdata/findImporters", tc.files) if tc.expectedErr != nil { require.EqualError(t, err, tc.expectedErr.Error()) @@ -248,7 +248,7 @@ func TestFindImportersForFiles(t *testing.T) { if filepath.Base(file) != jpath.DefaultEntrypoint { continue } - _, err := EvaluateFile(jsonnetImpl, file, Opts{}) + _, err := EvaluateFile(t.Context(), jsonnetImpl, file, Opts{}) require.NoError(t, err, "failed to eval %s", file) } @@ -299,7 +299,7 @@ testdata/findImporters/lib/lib1/subfolder/test.libsonnet: 0 } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - count, err := CountImporters("testdata/findImporters", tc.dir, tc.recursive, tc.fileRegexp) + count, err := CountImporters(t.Context(), "testdata/findImporters", tc.dir, tc.recursive, tc.fileRegexp) require.NoError(t, err) require.Equal(t, tc.expected, count) }) @@ -319,7 +319,7 @@ func BenchmarkFindImporters(b *testing.B) { importersCache = make(map[string][]string) jsonnetFilesCache = make(map[string]map[string]*cachedJsonnetFile) symlinkCache = make(map[string]string) - importers, err := FindImporterForFiles(tempDir, []string{filepath.Join(tempDir, "file10.libsonnet")}) + importers, err := FindImporterForFiles(b.Context(), tempDir, []string{filepath.Join(tempDir, "file10.libsonnet")}) require.NoError(b, err) require.Equal(b, expectedImporters, importers) diff --git a/pkg/jsonnet/imports.go b/pkg/jsonnet/imports.go index 59c27171d..c87d3df78 100644 --- a/pkg/jsonnet/imports.go +++ b/pkg/jsonnet/imports.go @@ -1,6 +1,7 @@ package jsonnet import ( + "context" "crypto/sha256" "encoding/base64" "fmt" @@ -22,7 +23,7 @@ import ( var importsRegexp = regexp.MustCompile(`import(str)?\s+['"]([^'"%()]+)['"]`) // TransitiveImports returns all recursive imports of an environment -func TransitiveImports(dir string) ([]string, error) { +func TransitiveImports(_ context.Context, dir string) ([]string, error) { dir, err := filepath.Abs(dir) if err != nil { return nil, err diff --git a/pkg/jsonnet/imports_test.go b/pkg/jsonnet/imports_test.go index 24e0ccd26..925450c21 100644 --- a/pkg/jsonnet/imports_test.go +++ b/pkg/jsonnet/imports_test.go @@ -16,7 +16,7 @@ import ( // TestTransitiveImports checks that TransitiveImports is able to report all // recursive imports of a file func TestTransitiveImports(t *testing.T) { - imports, err := TransitiveImports("testdata/importTree") + imports, err := TransitiveImports(t.Context(), "testdata/importTree") fmt.Println(imports) require.NoError(t, err) assert.Equal(t, []string{ diff --git a/pkg/jsonnet/jpath/jpath_test.go b/pkg/jsonnet/jpath/jpath_test.go index 31e31c33e..f2afbe123 100644 --- a/pkg/jsonnet/jpath/jpath_test.go +++ b/pkg/jsonnet/jpath/jpath_test.go @@ -14,7 +14,7 @@ import ( var jsonnetImpl = &goimpl.JsonnetGoImplementation{} func TestResolvePrecedence(t *testing.T) { - s, err := jsonnet.EvaluateFile(jsonnetImpl, "./testdata/precedence/environments/default/main.jsonnet", jsonnet.Opts{}) + s, err := jsonnet.EvaluateFile(t.Context(), jsonnetImpl, "./testdata/precedence/environments/default/main.jsonnet", jsonnet.Opts{}) require.NoError(t, err) want := map[string]string{ diff --git a/pkg/jsonnet/otel.go b/pkg/jsonnet/otel.go new file mode 100644 index 000000000..736426487 --- /dev/null +++ b/pkg/jsonnet/otel.go @@ -0,0 +1,5 @@ +package jsonnet + +import "github.com/grafana/tanka/internal/telemetry" + +var tracer = telemetry.Tracer("jsonnet") diff --git a/pkg/kubernetes/diff.go b/pkg/kubernetes/diff.go index 7eac9b902..c005b43f0 100644 --- a/pkg/kubernetes/diff.go +++ b/pkg/kubernetes/diff.go @@ -1,6 +1,7 @@ package kubernetes import ( + "context" "fmt" "github.com/Masterminds/semver" @@ -12,7 +13,9 @@ import ( ) // Diff takes the desired state and returns the differences from the cluster -func (k *Kubernetes) Diff(state manifest.List, opts DiffOpts) (*string, error) { +func (k *Kubernetes) Diff(ctx context.Context, state manifest.List, opts DiffOpts) (*string, error) { + _, span := tracer.Start(ctx, "kubernetes.Diff") + span.End() // prevent https://github.com/kubernetes/kubernetes/issues/89762 until fixed if k.ctl.Info().ClientVersion.Equal(semver.MustParse("1.18.0")) { return nil, fmt.Errorf(`you seem to be using kubectl 1.18.0, which contains an unfixed issue diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go index 8bbbb9bc5..842c54aa0 100644 --- a/pkg/kubernetes/kubernetes.go +++ b/pkg/kubernetes/kubernetes.go @@ -3,11 +3,14 @@ package kubernetes import ( "github.com/Masterminds/semver" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/kubernetes/client" "github.com/grafana/tanka/pkg/kubernetes/manifest" "github.com/grafana/tanka/pkg/spec/v1alpha1" ) +var tracer = telemetry.Tracer("kubernetes") + // Kubernetes exposes methods to work with the Kubernetes orchestrator type Kubernetes struct { Env v1alpha1.Environment diff --git a/pkg/tanka/evaluators.go b/pkg/tanka/evaluators.go index 796459ea3..701cd8ed6 100644 --- a/pkg/tanka/evaluators.go +++ b/pkg/tanka/evaluators.go @@ -1,6 +1,7 @@ package tanka import ( + "context" "fmt" "strings" @@ -12,7 +13,7 @@ import ( ) // EvalJsonnet evaluates the jsonnet environment at the given file system path -func evalJsonnet(path string, impl types.JsonnetImplementation, opts jsonnet.Opts) (raw string, err error) { +func evalJsonnet(ctx context.Context, path string, impl types.JsonnetImplementation, opts jsonnet.Opts) (raw string, err error) { entrypoint, err := jpath.Entrypoint(path) if err != nil { return "", err @@ -21,7 +22,7 @@ func evalJsonnet(path string, impl types.JsonnetImplementation, opts jsonnet.Opt // evaluate Jsonnet if opts.EvalScript != "" { // Determine if the entrypoint is a function. - isFunction, err := jsonnet.Evaluate(path, impl, fmt.Sprintf("std.isFunction(import '%s')", entrypoint), opts) + isFunction, err := jsonnet.Evaluate(ctx, path, impl, fmt.Sprintf("std.isFunction(import '%s')", entrypoint), opts) if err != nil { return "", fmt.Errorf("evaluating jsonnet in path '%s': %w", path, err) } @@ -43,14 +44,14 @@ function(%s) `, tlaJoin, entrypoint, tlaJoin, opts.EvalScript) } - raw, err = jsonnet.Evaluate(path, impl, evalScript, opts) + raw, err = jsonnet.Evaluate(ctx, path, impl, evalScript, opts) if err != nil { return "", fmt.Errorf("evaluating jsonnet in path '%s': %w", path, err) } return raw, nil } - raw, err = jsonnet.EvaluateFile(impl, entrypoint, opts) + raw, err = jsonnet.EvaluateFile(ctx, impl, entrypoint, opts) if err != nil { return "", errors.Wrap(err, "evaluating jsonnet") } diff --git a/pkg/tanka/evaluators_test.go b/pkg/tanka/evaluators_test.go index 0802a2152..acf4a6a3d 100644 --- a/pkg/tanka/evaluators_test.go +++ b/pkg/tanka/evaluators_test.go @@ -30,7 +30,7 @@ func TestEvalJsonnet(t *testing.T) { // This will fail intermittently if TLAs are passed as positional // parameters. - json, err := evalJsonnet("testdata/cases/withtlas", jsonnetImpl, opts) + json, err := evalJsonnet(t.Context(), "testdata/cases/withtlas", jsonnetImpl, opts) assert.NoError(t, err) assert.Equal(t, `"foovalue"`, strings.TrimSpace(json)) } @@ -46,7 +46,7 @@ func TestEvalJsonnetWithExpression(t *testing.T) { // This will fail intermittently if TLAs are passed as positional // parameters. - json, err := evalJsonnet("testdata/cases/object", jsonnetImpl, opts) + json, err := evalJsonnet(t.Context(), "testdata/cases/object", jsonnetImpl, opts) assert.NoError(t, err) assert.Equal(t, `"object"`, strings.TrimSpace(json)) }) @@ -59,7 +59,7 @@ func TestEvalWithOptionalTlas(t *testing.T) { opts := jsonnet.Opts{ EvalScript: "main.metadata.name", } - json, err := evalJsonnet("testdata/cases/with-optional-tlas/main.jsonnet", jsonnetImpl, opts) + json, err := evalJsonnet(t.Context(), "testdata/cases/with-optional-tlas/main.jsonnet", jsonnetImpl, opts) assert.NoError(t, err) assert.Equal(t, `"bar-baz"`, strings.TrimSpace(json)) } @@ -71,7 +71,7 @@ func TestEvalWithOptionalTlasSpecifiedArg2(t *testing.T) { EvalScript: "main.metadata.name", TLACode: jsonnet.InjectedCode{"baz": "'changed'"}, } - json, err := evalJsonnet("testdata/cases/with-optional-tlas/main.jsonnet", jsonnetImpl, opts) + json, err := evalJsonnet(t.Context(), "testdata/cases/with-optional-tlas/main.jsonnet", jsonnetImpl, opts) assert.NoError(t, err) assert.Equal(t, `"bar-changed"`, strings.TrimSpace(json)) } @@ -82,7 +82,7 @@ func TestEvalFunctionWithNoTlas(t *testing.T) { opts := jsonnet.Opts{ EvalScript: "main.metadata.name", } - json, err := evalJsonnet("testdata/cases/function-with-zero-params/main.jsonnet", jsonnetImpl, opts) + json, err := evalJsonnet(t.Context(), "testdata/cases/function-with-zero-params/main.jsonnet", jsonnetImpl, opts) assert.NoError(t, err) assert.Equal(t, `"inline"`, strings.TrimSpace(json)) } @@ -94,7 +94,7 @@ func TestInvalidTlaArg(t *testing.T) { EvalScript: "main", TLACode: jsonnet.InjectedCode{"foo": "'bar'"}, } - json, err := evalJsonnet("testdata/cases/function-with-zero-params/main.jsonnet", jsonnetImpl, opts) + json, err := evalJsonnet(t.Context(), "testdata/cases/function-with-zero-params/main.jsonnet", jsonnetImpl, opts) assert.Contains(t, err.Error(), "function has no parameter foo") assert.Equal(t, "", json) } @@ -106,7 +106,7 @@ func TestTlaWithNonFunction(t *testing.T) { EvalScript: "main", TLACode: jsonnet.InjectedCode{"foo": "'bar'"}, } - json, err := evalJsonnet("testdata/cases/withenv/main.jsonnet", jsonnetImpl, opts) + json, err := evalJsonnet(t.Context(), "testdata/cases/withenv/main.jsonnet", jsonnetImpl, opts) assert.NoError(t, err) assert.NotEmpty(t, json) } diff --git a/pkg/tanka/export.go b/pkg/tanka/export.go index 8346f4fca..67fac9fda 100644 --- a/pkg/tanka/export.go +++ b/pkg/tanka/export.go @@ -2,6 +2,7 @@ package tanka import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -16,6 +17,7 @@ import ( "github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/labels" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/kubernetes/manifest" "github.com/grafana/tanka/pkg/spec/v1alpha1" ) @@ -61,7 +63,12 @@ type ExportEnvOpts struct { MergeDeletedEnvs []string } -func ExportEnvironments(envs []*v1alpha1.Environment, to string, opts *ExportEnvOpts) error { +func ExportEnvironments(ctx context.Context, envs []*v1alpha1.Environment, to string, opts *ExportEnvOpts) error { + ctx, span := tracer.Start(ctx, "tanka.ExportEnvironments") + defer span.End() + + span.SetAttributes(telemetry.AttrNumEnvs(len(envs))) + // Keep track of which file maps to which environment fileToEnv := map[string]string{} @@ -87,7 +94,7 @@ func ExportEnvironments(envs []*v1alpha1.Environment, to string, opts *ExportEnv } // get all environments for paths - loadedEnvs, err := parallelLoadEnvironments(envs, parallelOpts{ + loadedEnvs, err := parallelLoadEnvironments(ctx, envs, parallelOpts{ Opts: opts.Opts, Selector: opts.Selector, Parallelism: opts.Parallelism, @@ -96,59 +103,65 @@ func ExportEnvironments(envs []*v1alpha1.Environment, to string, opts *ExportEnv return err } - for _, env := range loadedEnvs { - // get the manifests - loaded, err := LoadManifests(env, opts.Opts.Filters) - if err != nil { - return err - } - - env := loaded.Env - res := loaded.Resources - - // create raw manifest version of env for templating - env.Data = nil - raw, err := json.Marshal(env) - if err != nil { - return err - } - var menv manifest.Manifest - if err := json.Unmarshal(raw, &menv); err != nil { - return err - } - - // create template - manifestTemplate, err := createTemplate(opts.Format, menv) - if err != nil { - return fmt.Errorf("parsing format: %s", err) - } + { + ctx, span := tracer.Start(ctx, "generateManifests") + defer span.End() - // write each to a file - for _, m := range res { - // apply template - name, err := applyTemplate(manifestTemplate, m) + // FINDING: Generating the export files takes some time. Perhaps we should parallelize this. + for _, env := range loadedEnvs { + // get the manifests + loaded, err := LoadManifests(ctx, env, opts.Opts.Filters) if err != nil { - return fmt.Errorf("executing name template: %w", err) + return err } - // Create all subfolders in path - relpath := name + "." + opts.Extension - path := filepath.Join(to, relpath) - - fileToEnv[relpath] = env.Metadata.Namespace + env := loaded.Env + res := loaded.Resources - // Abort if already exists - if exists, err := fileExists(path); err != nil { + // create raw manifest version of env for templating + env.Data = nil + raw, err := json.Marshal(env) + if err != nil { return err - } else if exists { - return fmt.Errorf("file '%s' already exists. Aborting", path) } - - // Write manifest - data := m.String() - if err := writeExportFile(path, []byte(data)); err != nil { + var menv manifest.Manifest + if err := json.Unmarshal(raw, &menv); err != nil { return err } + + // create template + manifestTemplate, err := createTemplate(opts.Format, menv) + if err != nil { + return fmt.Errorf("parsing format: %s", err) + } + + // write each to a file + for _, m := range res { + // apply template + name, err := applyTemplate(manifestTemplate, m) + if err != nil { + return fmt.Errorf("executing name template: %w", err) + } + + // Create all subfolders in path + relpath := name + "." + opts.Extension + path := filepath.Join(to, relpath) + + fileToEnv[relpath] = env.Metadata.Namespace + + // Abort if already exists + if exists, err := fileExists(path); err != nil { + return err + } else if exists { + return fmt.Errorf("file '%s' already exists. Aborting", path) + } + + // Write manifest + data := m.String() + if err := writeExportFile(path, []byte(data)); err != nil { + return err + } + } } } diff --git a/pkg/tanka/export_test.go b/pkg/tanka/export_test.go index 7bf000d8b..276c9fe1e 100644 --- a/pkg/tanka/export_test.go +++ b/pkg/tanka/export_test.go @@ -50,7 +50,7 @@ func TestExportEnvironments(t *testing.T) { defer func() { require.NoError(t, os.Chdir("..")) }() // Find envs - envs, err := FindEnvs("test-export-envs", FindOpts{Selector: labels.Everything()}) + envs, err := FindEnvs(t.Context(), "test-export-envs", FindOpts{Selector: labels.Everything()}) require.NoError(t, err) // Export all envs @@ -62,7 +62,7 @@ func TestExportEnvironments(t *testing.T) { "deploymentName": "'initial-deployment'", "serviceName": "'initial-service'", } - require.NoError(t, ExportEnvironments(envs, tempDir, opts)) + require.NoError(t, ExportEnvironments(t.Context(), envs, tempDir, opts)) checkFiles(t, tempDir, []string{ filepath.Join(tempDir, "inline-namespace1", "my-configmap.yaml"), filepath.Join(tempDir, "inline-namespace1", "my-deployment.yaml"), @@ -86,11 +86,11 @@ func TestExportEnvironments(t *testing.T) { }`) // Try to re-export - assert.EqualError(t, ExportEnvironments(envs, tempDir, opts), fmt.Sprintf("output dir `%s` not empty. Pass a different --merge-strategy to ignore this", tempDir)) + assert.EqualError(t, ExportEnvironments(t.Context(), envs, tempDir, opts), fmt.Sprintf("output dir `%s` not empty. Pass a different --merge-strategy to ignore this", tempDir)) // Try to re-export with the --merge-strategy=fail-on-conflicts flag. Will still fail because Tanka will not overwrite manifests silently opts.MergeStrategy = ExportMergeStrategyFailConflicts - assert.ErrorContains(t, ExportEnvironments(envs, tempDir, opts), "already exists. Aborting") + assert.ErrorContains(t, ExportEnvironments(t.Context(), envs, tempDir, opts), "already exists. Aborting") // Re-export only one env with --merge-stategy=replace-envs flag opts.Opts.ExtCode = jsonnet.InjectedCode{ @@ -98,9 +98,9 @@ func TestExportEnvironments(t *testing.T) { "serviceName": "'updated-service'", } opts.MergeStrategy = ExportMergeStrategyReplaceEnvs - staticEnv, err := FindEnvs("test-export-envs", FindOpts{Selector: labels.SelectorFromSet(labels.Set{"type": "static"})}) + staticEnv, err := FindEnvs(t.Context(), "test-export-envs", FindOpts{Selector: labels.SelectorFromSet(labels.Set{"type": "static"})}) require.NoError(t, err) - require.NoError(t, ExportEnvironments(staticEnv, tempDir, opts)) + require.NoError(t, ExportEnvironments(t.Context(), staticEnv, tempDir, opts)) checkFiles(t, tempDir, []string{ filepath.Join(tempDir, "inline-namespace1", "my-configmap.yaml"), filepath.Join(tempDir, "inline-namespace1", "my-deployment.yaml"), @@ -129,7 +129,7 @@ func TestExportEnvironments(t *testing.T) { "serviceName": "'updated-again-service'", } opts.MergeDeletedEnvs = []string{"test-export-envs/inline-envs/main.jsonnet"} - require.NoError(t, ExportEnvironments(staticEnv, tempDir, opts)) + require.NoError(t, ExportEnvironments(t.Context(), staticEnv, tempDir, opts)) checkFiles(t, tempDir, []string{ filepath.Join(tempDir, "static", "updated-again-deployment.yaml"), filepath.Join(tempDir, "static", "updated-again-service.yaml"), @@ -149,7 +149,7 @@ func TestExportEnvironmentsBroken(t *testing.T) { defer func() { require.NoError(t, os.Chdir("..")) }() // Find envs - envs, err := FindEnvs("test-export-envs-broken", FindOpts{Selector: labels.Everything()}) + envs, err := FindEnvs(t.Context(), "test-export-envs-broken", FindOpts{Selector: labels.Everything()}) require.NoError(t, err) // Export all envs @@ -159,7 +159,7 @@ func TestExportEnvironmentsBroken(t *testing.T) { } var schemaError *manifest.SchemaError - require.ErrorAs(t, ExportEnvironments(envs, tempDir, opts), &schemaError) + require.ErrorAs(t, ExportEnvironments(t.Context(), envs, tempDir, opts), &schemaError) } func BenchmarkExportEnvironmentsWithReplaceEnvs(b *testing.B) { @@ -169,7 +169,7 @@ func BenchmarkExportEnvironmentsWithReplaceEnvs(b *testing.B) { defer func() { require.NoError(b, os.Chdir("..")) }() // Find envs - envs, err := FindEnvs("test-export-envs", FindOpts{Selector: labels.Everything()}) + envs, err := FindEnvs(b.Context(), "test-export-envs", FindOpts{Selector: labels.Everything()}) require.NoError(b, err) // Export all envs @@ -183,12 +183,12 @@ func BenchmarkExportEnvironmentsWithReplaceEnvs(b *testing.B) { "serviceName": "'initial-service'", } // Export a first time so that the benchmark loops are identical - require.NoError(b, ExportEnvironments(envs, tempDir, opts)) + require.NoError(b, ExportEnvironments(b.Context(), envs, tempDir, opts)) // On every loop, delete manifests from previous envs + reexport all envs b.ResetTimer() for i := 0; i < b.N; i++ { - require.NoError(b, ExportEnvironments(envs, tempDir, opts), "failed on iteration %d", i) + require.NoError(b, ExportEnvironments(b.Context(), envs, tempDir, opts), "failed on iteration %d", i) } } diff --git a/pkg/tanka/find.go b/pkg/tanka/find.go index 4dad7b3ae..2d6d2c440 100644 --- a/pkg/tanka/find.go +++ b/pkg/tanka/find.go @@ -1,11 +1,13 @@ package tanka import ( + "context" "fmt" "path/filepath" "runtime" "time" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/jsonnet" "github.com/grafana/tanka/pkg/jsonnet/jpath" "github.com/grafana/tanka/pkg/spec/v1alpha1" @@ -26,16 +28,18 @@ type FindOpts struct { // Each directory is tested and included if it is a valid environment, either // static or inline. If a directory is a valid environment, its subdirectories // are not checked. -func FindEnvs(path string, opts FindOpts) ([]*v1alpha1.Environment, error) { - return findEnvsFromPaths([]string{path}, opts) +func FindEnvs(ctx context.Context, path string, opts FindOpts) ([]*v1alpha1.Environment, error) { + return findEnvsFromPaths(ctx, []string{path}, opts) } // FindEnvsFromPaths does the same as FindEnvs but takes a list of paths instead -func FindEnvsFromPaths(paths []string, opts FindOpts) ([]*v1alpha1.Environment, error) { - return findEnvsFromPaths(paths, opts) +func FindEnvsFromPaths(ctx context.Context, paths []string, opts FindOpts) ([]*v1alpha1.Environment, error) { + return findEnvsFromPaths(ctx, paths, opts) } -func findEnvsFromPaths(paths []string, opts FindOpts) ([]*v1alpha1.Environment, error) { +func findEnvsFromPaths(ctx context.Context, paths []string, opts FindOpts) ([]*v1alpha1.Environment, error) { + ctx, span := tracer.Start(ctx, "tanka.findEnvsFromPaths") + defer span.End() if opts.Parallelism <= 0 { opts.Parallelism = runtime.NumCPU() } @@ -45,14 +49,18 @@ func findEnvsFromPaths(paths []string, opts FindOpts) ([]*v1alpha1.Environment, jsonnetFiles, err := findJsonnetFilesFromPaths(paths, opts) if err != nil { - return nil, fmt.Errorf("finding jsonnet files: %w", err) + err = fmt.Errorf("finding jsonnet files: %w", err) + telemetry.FailSpanWithError(span, err) + return nil, err } findJsonnetFilesEndTime := time.Now() - envs, err := findEnvsFromJsonnetFiles(jsonnetFiles, opts) + envs, err := findEnvsFromJsonnetFiles(ctx, jsonnetFiles, opts) if err != nil { - return nil, fmt.Errorf("finding environments: %w", err) + err = fmt.Errorf("finding environments: %w", err) + telemetry.FailSpanWithError(span, err) + return nil, err } findEnvsEndTime := time.Now() @@ -117,7 +125,7 @@ func findJsonnetFilesFromPaths(paths []string, opts FindOpts) ([]string, error) } // find all environments within jsonnet files -func findEnvsFromJsonnetFiles(jsonnetFiles []string, opts FindOpts) ([]*v1alpha1.Environment, error) { +func findEnvsFromJsonnetFiles(ctx context.Context, jsonnetFiles []string, opts FindOpts) ([]*v1alpha1.Environment, error) { type findEnvsOut struct { envs []*v1alpha1.Environment err error @@ -134,7 +142,7 @@ func findEnvsFromJsonnetFiles(jsonnetFiles []string, opts FindOpts) ([]*v1alpha1 for jsonnetFile := range jsonnetFilesChan { // try if this has envs - list, err := List(jsonnetFile, Opts{JsonnetOpts: jsonnetOpts, JsonnetImplementation: opts.JsonnetImplementation}) + list, err := List(ctx, jsonnetFile, Opts{JsonnetOpts: jsonnetOpts, JsonnetImplementation: opts.JsonnetImplementation}) if err != nil && // expected when looking for environments !errors.As(err, &jpath.ErrorNoBase{}) && diff --git a/pkg/tanka/find_test.go b/pkg/tanka/find_test.go index 5f2f20a03..381a4eea4 100644 --- a/pkg/tanka/find_test.go +++ b/pkg/tanka/find_test.go @@ -15,7 +15,7 @@ func BenchmarkFindEnvsFromSinglePath(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - envs, err := FindEnvs(tempDir, FindOpts{}) + envs, err := FindEnvs(b.Context(), tempDir, FindOpts{}) require.Len(b, envs, 200) require.NoError(b, err) } @@ -27,7 +27,7 @@ func BenchmarkFindEnvsFromPaths(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - envs, err := FindEnvsFromPaths(envPaths, FindOpts{}) + envs, err := FindEnvsFromPaths(b.Context(), envPaths, FindOpts{}) require.Len(b, envs, 200) require.NoError(b, err) } diff --git a/pkg/tanka/inline.go b/pkg/tanka/inline.go index aa614aca3..75d21cf9c 100644 --- a/pkg/tanka/inline.go +++ b/pkg/tanka/inline.go @@ -1,6 +1,7 @@ package tanka import ( + "context" "encoding/json" "fmt" "path/filepath" @@ -21,25 +22,29 @@ type InlineLoader struct { jsonnetImpl types.JsonnetImplementation } -func (i *InlineLoader) Load(path string, opts LoaderOpts) (*v1alpha1.Environment, error) { +func (i *InlineLoader) Name() string { + return "inline" +} + +func (i *InlineLoader) Load(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) { if opts.Name != "" { opts.JsonnetOpts.EvalScript = fmt.Sprintf(SingleEnvEvalScript, opts.Name) } - return i.load(path, opts) + return i.load(ctx, path, opts) } -func (i *InlineLoader) Peek(path string, opts LoaderOpts) (*v1alpha1.Environment, error) { +func (i *InlineLoader) Peek(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) { opts.JsonnetOpts.EvalScript = MetadataEvalScript if opts.Name != "" { opts.JsonnetOpts.EvalScript = fmt.Sprintf(MetadataSingleEnvEvalScript, opts.Name) } - env, err := i.load(path, opts) + env, err := i.load(ctx, path, opts) return env, err } // abstracted out as Peek and Load need different JsonnetOpts -func (i *InlineLoader) load(path string, opts LoaderOpts) (*v1alpha1.Environment, error) { - data, err := i.Eval(path, opts) +func (i *InlineLoader) load(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) { + data, err := i.Eval(ctx, path, opts) if err != nil { return nil, err } @@ -84,9 +89,9 @@ func (i *InlineLoader) load(path string, opts LoaderOpts) (*v1alpha1.Environment return env, nil } -func (i *InlineLoader) List(path string, opts LoaderOpts) ([]*v1alpha1.Environment, error) { +func (i *InlineLoader) List(ctx context.Context, path string, opts LoaderOpts) ([]*v1alpha1.Environment, error) { opts.JsonnetOpts.EvalScript = MetadataEvalScript - data, err := i.Eval(path, opts) + data, err := i.Eval(ctx, path, opts) if err != nil { return nil, err } @@ -114,11 +119,11 @@ func (i *InlineLoader) List(path string, opts LoaderOpts) ([]*v1alpha1.Environme return envs, nil } -func (i *InlineLoader) Eval(path string, opts LoaderOpts) (interface{}, error) { +func (i *InlineLoader) Eval(ctx context.Context, path string, opts LoaderOpts) (interface{}, error) { // Can't provide env as extVar, as we need to evaluate Jsonnet first to know it opts.ExtCode.Set(environmentExtCode, `error "Using tk.env and std.extVar('tanka.dev/environment') is only supported for static environments. Directly access this data using standard Jsonnet instead."`) - raw, err := evalJsonnet(path, i.jsonnetImpl, opts.JsonnetOpts) + raw, err := evalJsonnet(ctx, path, i.jsonnetImpl, opts.JsonnetOpts) if err != nil { return nil, err } diff --git a/pkg/tanka/load.go b/pkg/tanka/load.go index 0721c6f7b..aa1bbe08f 100644 --- a/pkg/tanka/load.go +++ b/pkg/tanka/load.go @@ -1,11 +1,13 @@ package tanka import ( + "context" "fmt" "os" "path/filepath" "strings" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/jsonnet/implementations/binary" "github.com/grafana/tanka/pkg/jsonnet/implementations/goimpl" "github.com/grafana/tanka/pkg/jsonnet/implementations/types" @@ -17,6 +19,7 @@ import ( "github.com/grafana/tanka/pkg/spec/v1alpha1" "github.com/pkg/errors" "github.com/rs/zerolog/log" + "go.opentelemetry.io/otel/attribute" ) // environmentExtCode is the extCode ID `tk.env` uses underneath @@ -25,13 +28,13 @@ const environmentExtCode = spec.APIGroup + "/environment" // Load loads the Environment at `path`. It automatically detects whether to // load inline or statically -func Load(path string, opts Opts) (*LoadResult, error) { - env, err := LoadEnvironment(path, opts) +func Load(ctx context.Context, path string, opts Opts) (*LoadResult, error) { + env, err := LoadEnvironment(ctx, path, opts) if err != nil { return nil, err } - result, err := LoadManifests(env, opts.Filters) + result, err := LoadManifests(ctx, env, opts.Filters) if err != nil { return nil, err } @@ -45,7 +48,11 @@ func Load(path string, opts Opts) (*LoadResult, error) { return result, nil } -func LoadEnvironment(path string, opts Opts) (*v1alpha1.Environment, error) { +func LoadEnvironment(ctx context.Context, path string, opts Opts) (*v1alpha1.Environment, error) { + ctx, span := tracer.Start(ctx, "tanka.LoadEnvironment") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path), attribute.String("tanka.nameFilter", opts.Name)) + _, err := os.Stat(path) if os.IsNotExist(err) { log.Info().Msgf("Path %q does not exist, trying to use it as an environment name", path) @@ -59,8 +66,9 @@ func LoadEnvironment(path string, opts Opts) (*v1alpha1.Environment, error) { if err != nil { return nil, err } + span.SetAttributes(telemetry.AttrLoader(loader.Name())) - env, err := loader.Load(path, LoaderOpts{opts.JsonnetOpts, opts.Name}) + env, err := loader.Load(ctx, path, LoaderOpts{opts.JsonnetOpts, opts.Name}) if err != nil { return nil, err } @@ -68,7 +76,11 @@ func LoadEnvironment(path string, opts Opts) (*v1alpha1.Environment, error) { return env, nil } -func LoadManifests(env *v1alpha1.Environment, filters process.Matchers) (*LoadResult, error) { +func LoadManifests(ctx context.Context, env *v1alpha1.Environment, filters process.Matchers) (*LoadResult, error) { + _, span := tracer.Start(ctx, "tanka.LoadManifests") + defer span.End() + span.SetAttributes(telemetry.AttrEnv(env)...) + if err := checkVersion(env.Spec.ExpectVersions.Tanka); err != nil { return nil, err } @@ -83,25 +95,34 @@ func LoadManifests(env *v1alpha1.Environment, filters process.Matchers) (*LoadRe // Peek loads the metadata of the environment at path. To get resources as well, // use Load -func Peek(path string, opts Opts) (*v1alpha1.Environment, error) { +func Peek(ctx context.Context, path string, opts Opts) (*v1alpha1.Environment, error) { + ctx, span := tracer.Start(ctx, "tanka.Peek") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path)) + loader, err := DetectLoader(path, opts) if err != nil { return nil, err } + span.SetAttributes(telemetry.AttrLoader(loader.Name())) - return loader.Peek(path, LoaderOpts{opts.JsonnetOpts, opts.Name}) + return loader.Peek(ctx, path, LoaderOpts{opts.JsonnetOpts, opts.Name}) } // List finds metadata of all environments at path that could possibly be // loaded. List can be used to deal with multiple inline environments, by first // listing them, choosing the right one and then only loading that one -func List(path string, opts Opts) ([]*v1alpha1.Environment, error) { +func List(ctx context.Context, path string, opts Opts) ([]*v1alpha1.Environment, error) { + ctx, span := tracer.Start(ctx, "tanka.List") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path)) loader, err := DetectLoader(path, opts) if err != nil { return nil, err } + span.SetAttributes(telemetry.AttrLoader(loader.Name())) - return loader.List(path, LoaderOpts{opts.JsonnetOpts, opts.Name}) + return loader.List(ctx, path, LoaderOpts{opts.JsonnetOpts, opts.Name}) } func getJsonnetImplementation(path string, opts Opts) (types.JsonnetImplementation, error) { @@ -133,13 +154,17 @@ func getJsonnetImplementation(path string, opts Opts) (types.JsonnetImplementati } // Eval returns the raw evaluated Jsonnet -func Eval(path string, opts Opts) (interface{}, error) { +func Eval(ctx context.Context, path string, opts Opts) (interface{}, error) { + ctx, span := tracer.Start(ctx, "tanka.Eval") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path)) loader, err := DetectLoader(path, opts) if err != nil { return nil, err } + span.SetAttributes(telemetry.AttrLoader(loader.Name())) - return loader.Eval(path, LoaderOpts{opts.JsonnetOpts, opts.Name}) + return loader.Eval(ctx, path, LoaderOpts{opts.JsonnetOpts, opts.Name}) } // DetectLoader detects whether the environment is inline or static and picks @@ -172,18 +197,21 @@ func DetectLoader(path string, opts Opts) (Loader, error) { // Loader is an abstraction over the process of loading Environments type Loader interface { + // Name of the loader + Name() string + // Load a single environment at path - Load(path string, opts LoaderOpts) (*v1alpha1.Environment, error) + Load(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) // Peek only loads metadata and omits the actual resources - Peek(path string, opts LoaderOpts) (*v1alpha1.Environment, error) + Peek(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) // List returns metadata of all possible environments at path that can be // loaded - List(path string, opts LoaderOpts) ([]*v1alpha1.Environment, error) + List(ctx context.Context, path string, opts LoaderOpts) ([]*v1alpha1.Environment, error) // Eval returns the raw evaluated Jsonnet - Eval(path string, opts LoaderOpts) (interface{}, error) + Eval(ctx context.Context, path string, opts LoaderOpts) (interface{}, error) } type LoaderOpts struct { @@ -191,6 +219,13 @@ type LoaderOpts struct { Name string } +func OTELAttrFromLoaderOpts(opts *LoaderOpts) []attribute.KeyValue { + result := make([]attribute.KeyValue, 0, 2) + result = append(result, attribute.String("tanka.loader.options.name", opts.Name)) + result = append(result, attribute.String("tanka.loader.options.cache_path", opts.CachePath)) + return result +} + type LoadResult struct { Env *v1alpha1.Environment Resources manifest.List diff --git a/pkg/tanka/load_test.go b/pkg/tanka/load_test.go index b2f44d6fc..9843b6ef6 100644 --- a/pkg/tanka/load_test.go +++ b/pkg/tanka/load_test.go @@ -147,7 +147,7 @@ func TestLoad(t *testing.T) { for _, test := range cases { t.Run(test.name, func(t *testing.T) { - l, err := Load(test.baseDir, Opts{}) + l, err := Load(t.Context(), test.baseDir, Opts{}) require.NoError(t, err) assert.Equal(t, test.expected, l.Resources) @@ -158,24 +158,24 @@ func TestLoad(t *testing.T) { func TestLoadSelectEnvironment(t *testing.T) { // No match - _, err := Load("./testdata/cases/multiple-inline-envs", Opts{Name: "no match"}) + _, err := Load(t.Context(), "./testdata/cases/multiple-inline-envs", Opts{Name: "no match"}) assert.EqualError(t, err, "found no matching environments; run 'tk env list ./testdata/cases/multiple-inline-envs' to view available options") // Empty options, match all environments - _, err = Load("./testdata/cases/multiple-inline-envs", Opts{}) + _, err = Load(t.Context(), "./testdata/cases/multiple-inline-envs", Opts{}) assert.EqualError(t, err, "found multiple Environments in \"./testdata/cases/multiple-inline-envs\". Use `--name` to select a single one: \n - project1-env1\n - project1-env2\n - project2-env1") // Partial match two environments - _, err = Load("./testdata/cases/multiple-inline-envs", Opts{Name: "env1"}) + _, err = Load(t.Context(), "./testdata/cases/multiple-inline-envs", Opts{Name: "env1"}) assert.EqualError(t, err, "found multiple Environments in \"./testdata/cases/multiple-inline-envs\" matching \"env1\". Provide a more specific name that matches a single one: \n - project1-env1\n - project2-env1") // Partial match - result, err := Load("./testdata/cases/multiple-inline-envs", Opts{Name: "project2"}) + result, err := Load(t.Context(), "./testdata/cases/multiple-inline-envs", Opts{Name: "project2"}) assert.NoError(t, err) assert.Equal(t, "project2-env1", result.Env.Metadata.Name) // Full match - result, err = Load("./testdata/cases/multiple-inline-envs", Opts{Name: "project1-env1"}) + result, err = Load(t.Context(), "./testdata/cases/multiple-inline-envs", Opts{Name: "project1-env1"}) assert.NoError(t, err) assert.Equal(t, "project1-env1", result.Env.Metadata.Name) } @@ -190,16 +190,16 @@ func TestLoadEnvironmentFallbackToName(t *testing.T) { defer func() { require.NoError(t, os.Chdir(cwd)) }() // Partial match two environments - _, err = Load("env1", Opts{}) + _, err = Load(t.Context(), "env1", Opts{}) assert.EqualError(t, err, "found multiple Environments in \".\" matching \"env1\". Provide a more specific name that matches a single one: \n - project1-env1\n - project2-env1") // Partial match - result, err := Load("project2", Opts{}) + result, err := Load(t.Context(), "project2", Opts{}) require.NoError(t, err) assert.Equal(t, "project2-env1", result.Env.Metadata.Name) // Full match - result, err = Load("project1-env1", Opts{}) + result, err = Load(t.Context(), "project1-env1", Opts{}) require.NoError(t, err) assert.Equal(t, "project1-env1", result.Env.Metadata.Name) } @@ -207,13 +207,13 @@ func TestLoadEnvironmentFallbackToName(t *testing.T) { func TestLoadSelectEnvironmentFullMatchHasPriority(t *testing.T) { // `base` matches both `base` and `base-and-more` // However, the full match should win - result, err := Load("./testdata/cases/inline-name-conflict", Opts{Name: "base"}) + result, err := Load(t.Context(), "./testdata/cases/inline-name-conflict", Opts{Name: "base"}) assert.NoError(t, err) assert.Equal(t, "base", result.Env.Metadata.Name) } func TestLoadFailsWhenBothSpecAndInline(t *testing.T) { - _, err := Load("./testdata/cases/static-and-inline", Opts{Name: "inline"}) + _, err := Load(t.Context(), "./testdata/cases/static-and-inline", Opts{Name: "inline"}) assert.EqualError(t, err, "found a tanka Environment resource. Check that you aren't using a spec.json and inline environments simultaneously") } diff --git a/pkg/tanka/parallel.go b/pkg/tanka/parallel.go index 483e07391..ae34e043f 100644 --- a/pkg/tanka/parallel.go +++ b/pkg/tanka/parallel.go @@ -1,6 +1,7 @@ package tanka import ( + "context" "fmt" "path/filepath" "time" @@ -22,7 +23,10 @@ type parallelOpts struct { } // parallelLoadEnvironments evaluates multiple environments in parallel -func parallelLoadEnvironments(envs []*v1alpha1.Environment, opts parallelOpts) ([]*v1alpha1.Environment, error) { +func parallelLoadEnvironments(ctx context.Context, envs []*v1alpha1.Environment, opts parallelOpts) ([]*v1alpha1.Environment, error) { + ctx, span := tracer.Start(ctx, "tanka.parallelLoadEnvironments") + defer span.End() + jobsCh := make(chan parallelJob) outCh := make(chan parallelOut, len(envs)) @@ -36,7 +40,7 @@ func parallelLoadEnvironments(envs []*v1alpha1.Environment, opts parallelOpts) ( } for i := 0; i < opts.Parallelism; i++ { - go parallelWorker(jobsCh, outCh) + go parallelWorker(ctx, jobsCh, outCh) } for _, env := range envs { @@ -100,12 +104,14 @@ type parallelOut struct { err error } -func parallelWorker(jobsCh <-chan parallelJob, outCh chan parallelOut) { +func parallelWorker(ctx context.Context, jobsCh <-chan parallelJob, outCh chan parallelOut) { + ctx, span := tracer.Start(ctx, "tanka.parallelWorker") + defer span.End() for job := range jobsCh { log.Debug().Str("name", job.opts.Name).Str("path", job.path).Msg("Loading environment") startTime := time.Now() - env, err := LoadEnvironment(job.path, job.opts) + env, err := LoadEnvironment(ctx, job.path, job.opts) if err != nil { err = fmt.Errorf("%s:\n %w", job.path, err) } diff --git a/pkg/tanka/prune.go b/pkg/tanka/prune.go index a377f0a81..ae3f71106 100644 --- a/pkg/tanka/prune.go +++ b/pkg/tanka/prune.go @@ -1,6 +1,7 @@ package tanka import ( + "context" "fmt" "os" @@ -17,9 +18,9 @@ type PruneOpts struct { // Prune deletes all resources from the cluster, that are no longer present in // Jsonnet. It uses the `tanka.dev/environment` label to identify those. -func Prune(baseDir string, opts PruneOpts) error { +func Prune(ctx context.Context, baseDir string, opts PruneOpts) error { // parse jsonnet, init k8s client - p, err := Load(baseDir, opts.Opts) + p, err := Load(ctx, baseDir, opts.Opts) if err != nil { return err } diff --git a/pkg/tanka/static.go b/pkg/tanka/static.go index d978b283c..178959912 100644 --- a/pkg/tanka/static.go +++ b/pkg/tanka/static.go @@ -1,6 +1,7 @@ package tanka import ( + "context" "encoding/json" "github.com/grafana/tanka/pkg/jsonnet/implementations/types" @@ -15,13 +16,17 @@ type StaticLoader struct { jsonnetImpl types.JsonnetImplementation } -func (s StaticLoader) Load(path string, opts LoaderOpts) (*v1alpha1.Environment, error) { - config, err := s.Peek(path, opts) +func (s StaticLoader) Name() string { + return "static" +} + +func (s StaticLoader) Load(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) { + config, err := s.Peek(ctx, path, opts) if err != nil { return nil, err } - data, err := s.Eval(path, opts) + data, err := s.Eval(ctx, path, opts) if err != nil { return nil, err } @@ -30,7 +35,7 @@ func (s StaticLoader) Load(path string, opts LoaderOpts) (*v1alpha1.Environment, return config, nil } -func (s StaticLoader) Peek(path string, _ LoaderOpts) (*v1alpha1.Environment, error) { +func (s StaticLoader) Peek(_ context.Context, path string, _ LoaderOpts) (*v1alpha1.Environment, error) { config, err := parseStaticSpec(path) if err != nil { return nil, err @@ -39,8 +44,8 @@ func (s StaticLoader) Peek(path string, _ LoaderOpts) (*v1alpha1.Environment, er return config, nil } -func (s StaticLoader) List(path string, opts LoaderOpts) ([]*v1alpha1.Environment, error) { - env, err := s.Peek(path, opts) +func (s StaticLoader) List(ctx context.Context, path string, opts LoaderOpts) ([]*v1alpha1.Environment, error) { + env, err := s.Peek(ctx, path, opts) if err != nil { return nil, err } @@ -48,8 +53,8 @@ func (s StaticLoader) List(path string, opts LoaderOpts) ([]*v1alpha1.Environmen return []*v1alpha1.Environment{env}, nil } -func (s *StaticLoader) Eval(path string, opts LoaderOpts) (interface{}, error) { - config, err := s.Peek(path, opts) +func (s *StaticLoader) Eval(ctx context.Context, path string, opts LoaderOpts) (interface{}, error) { + config, err := s.Peek(ctx, path, opts) if err != nil { return nil, err } @@ -60,7 +65,7 @@ func (s *StaticLoader) Eval(path string, opts LoaderOpts) (interface{}, error) { } opts.ExtCode.Set(environmentExtCode, envCode) - raw, err := evalJsonnet(path, s.jsonnetImpl, opts.JsonnetOpts) + raw, err := evalJsonnet(ctx, path, s.jsonnetImpl, opts.JsonnetOpts) if err != nil { return nil, err } diff --git a/pkg/tanka/status.go b/pkg/tanka/status.go index eb8cdb659..756d8aa73 100644 --- a/pkg/tanka/status.go +++ b/pkg/tanka/status.go @@ -1,6 +1,8 @@ package tanka import ( + "context" + "github.com/grafana/tanka/pkg/kubernetes/client" "github.com/grafana/tanka/pkg/kubernetes/manifest" "github.com/grafana/tanka/pkg/spec/v1alpha1" @@ -16,8 +18,8 @@ type Info struct { } // Status returns information about the particular environment -func Status(baseDir string, opts Opts) (*Info, error) { - r, err := Load(baseDir, opts) +func Status(ctx context.Context, baseDir string, opts Opts) (*Info, error) { + r, err := Load(ctx, baseDir, opts) if err != nil { return nil, err } diff --git a/pkg/tanka/tanka.go b/pkg/tanka/tanka.go index eee730ce7..7a85ad61c 100644 --- a/pkg/tanka/tanka.go +++ b/pkg/tanka/tanka.go @@ -9,6 +9,7 @@ import ( "github.com/Masterminds/semver" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/jsonnet" "github.com/grafana/tanka/pkg/process" ) @@ -31,6 +32,8 @@ type Opts struct { // provided using ldflags const defaultDevVersion = "dev" +var tracer = telemetry.Tracer("tanka") + // CurrentVersion is the current version of the running Tanka code var CurrentVersion = defaultDevVersion diff --git a/pkg/tanka/workflow.go b/pkg/tanka/workflow.go index b965e2b85..830639e46 100644 --- a/pkg/tanka/workflow.go +++ b/pkg/tanka/workflow.go @@ -1,6 +1,7 @@ package tanka import ( + "context" "fmt" "os" @@ -73,8 +74,8 @@ func (e ErrorApplyStrategyUnknown) Error() string { // Apply parses the environment at the given directory (a `baseDir`) and applies // the evaluated jsonnet to the Kubernetes cluster defined in the environments // `spec.json`. -func Apply(baseDir string, opts ApplyOpts) error { - l, err := Load(baseDir, opts.Opts) +func Apply(ctx context.Context, baseDir string, opts ApplyOpts) error { + l, err := Load(ctx, baseDir, opts.Opts) if err != nil { return err } @@ -105,7 +106,7 @@ func Apply(baseDir string, opts ApplyOpts) error { var noChanges bool if opts.DiffStrategy != "none" { // show diff - diff, err := kube.Diff(l.Resources, kubernetes.DiffOpts{Strategy: opts.DiffStrategy}) + diff, err := kube.Diff(ctx, l.Resources, kubernetes.DiffOpts{Strategy: opts.DiffStrategy}) switch { case err != nil: // This is not fatal, the diff is not strictly required @@ -176,8 +177,8 @@ type DiffOpts struct { // `WithDiffSummarize` modifier is used, a histogram is returned instead. // The cluster information is retrieved from the environments `spec.json`. // NOTE: This function requires on `diff(1)` and `kubectl(1)` -func Diff(baseDir string, opts DiffOpts) (*string, error) { - l, err := Load(baseDir, opts.Opts) +func Diff(ctx context.Context, baseDir string, opts DiffOpts) (*string, error) { + l, err := Load(ctx, baseDir, opts.Opts) if err != nil { return nil, err } @@ -187,7 +188,7 @@ func Diff(baseDir string, opts DiffOpts) (*string, error) { } defer kube.Close() - return kube.Diff(l.Resources, kubernetes.DiffOpts{ + return kube.Diff(ctx, l.Resources, kubernetes.DiffOpts{ Summarize: opts.Summarize, Strategy: opts.Strategy, WithPrune: opts.WithPrune, @@ -202,8 +203,8 @@ type DeleteOpts struct { // Delete parses the environment at the given directory (a `baseDir`) and deletes // the generated objects from the Kubernetes cluster defined in the environment's // `spec.json`. -func Delete(baseDir string, opts DeleteOpts) error { - l, err := Load(baseDir, opts.Opts) +func Delete(ctx context.Context, baseDir string, opts DeleteOpts) error { + l, err := Load(ctx, baseDir, opts.Opts) if err != nil { return err } @@ -245,8 +246,8 @@ func Delete(baseDir string, opts DeleteOpts) error { // Show parses the environment at the given directory (a `baseDir`) and returns // the list of Kubernetes objects. // Tip: use the `String()` function on the returned list to get the familiar yaml stream -func Show(baseDir string, opts Opts) (manifest.List, error) { - l, err := Load(baseDir, opts) +func Show(ctx context.Context, baseDir string, opts Opts) (manifest.List, error) { + l, err := Load(ctx, baseDir, opts) if err != nil { return nil, err }