diff --git a/go.mod b/go.mod index 0fadb89..34ff710 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 github.com/oklog/run v1.1.0 github.com/panta/machineid v1.0.2 - github.com/signadot/go-sdk v0.3.8-0.20240516163659-87dfff7a3922 + github.com/signadot/go-sdk v0.3.8-0.20240529233641-955e0237e964 github.com/signadot/libconnect v0.1.1-0.20240306100356-4c865b888453 github.com/spf13/cobra v1.6.0 github.com/spf13/viper v1.11.0 @@ -38,8 +38,10 @@ require ( github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/golang-jwt/jwt/v4 v4.2.0 // indirect github.com/mwitkow/grpc-proxy v0.0.0-20230212185441-f345521cb9c9 // indirect + github.com/r3labs/sse/v2 v2.10.0 // indirect golang.org/x/sync v0.7.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 // indirect + gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect k8s.io/kubectl v0.25.12 // indirect ) @@ -49,7 +51,7 @@ require ( github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/fatih/color v1.15.0 github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.23.0 // indirect github.com/go-openapi/errors v0.22.0 // indirect @@ -91,9 +93,9 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect go.mongodb.org/mongo-driver v1.15.0 // indirect - go.opentelemetry.io/otel v1.26.0 // indirect - go.opentelemetry.io/otel/metric v1.26.0 // indirect - go.opentelemetry.io/otel/trace v1.26.0 // indirect + go.opentelemetry.io/otel v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.27.0 // indirect + go.opentelemetry.io/otel/trace v1.27.0 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sys v0.18.0 // indirect @@ -118,4 +120,4 @@ require ( // Used for local dev // replace github.com/signadot/libconnect => ../libconnect/ -// replace github.com/signadot/go-sdk => ../go-sdk +//replace github.com/signadot/go-sdk => ../go-sdk diff --git a/go.sum b/go.sum index 5ee88d7..ff60083 100644 --- a/go.sum +++ b/go.sum @@ -103,6 +103,8 @@ github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= @@ -292,12 +294,16 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= +github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/signadot/go-sdk v0.3.7 h1:r8OuhbKzV9a55o7ZTkWuANGbwljfAcZBSwOqstN+tj8= +github.com/signadot/go-sdk v0.3.7/go.mod h1:O27LoqeYQzLmIUVCZ/dTukJSEtmZMnqP1Upp7I00tEg= github.com/signadot/go-sdk v0.3.8-0.20240514115106-afd906e78099 h1:BjcaLV9xxB7J5F8UW72Y1bUUVe1k2p5XONpXbjPaEN8= github.com/signadot/go-sdk v0.3.8-0.20240514115106-afd906e78099/go.mod h1:dgIOK+FoU5oEWHXYFI4diXJtBLVJTWsivUl7u8D5eCQ= github.com/signadot/go-sdk v0.3.8-0.20240514201336-f8c37ac3c72e h1:yfgTUgk4TtvZx7OGrBxY8HN4WTi0098p6m//8/eiJrE= @@ -306,6 +312,16 @@ github.com/signadot/go-sdk v0.3.8-0.20240516151729-61be7eb1919a h1:3fEzB8edKrTo4 github.com/signadot/go-sdk v0.3.8-0.20240516151729-61be7eb1919a/go.mod h1:dgIOK+FoU5oEWHXYFI4diXJtBLVJTWsivUl7u8D5eCQ= github.com/signadot/go-sdk v0.3.8-0.20240516163659-87dfff7a3922 h1:IBlllSFc/DQk33JzVI0RkFZ9/KlaCa8RWiKAfnO78hM= github.com/signadot/go-sdk v0.3.8-0.20240516163659-87dfff7a3922/go.mod h1:dgIOK+FoU5oEWHXYFI4diXJtBLVJTWsivUl7u8D5eCQ= +github.com/signadot/go-sdk v0.3.8-0.20240521210827-dc1d01e5e52b h1:0Qwmpo4x9vCtqnbMeoIirblxWfHOXL5y42HJepgZtwc= +github.com/signadot/go-sdk v0.3.8-0.20240521210827-dc1d01e5e52b/go.mod h1:LBc3zdVqtLQpXo78HN/DrMhf6PBcfyhlg/tVRrDL+sg= +github.com/signadot/go-sdk v0.3.8-0.20240522192013-3882db39d258 h1:ilo+iUvNMfCDxaS/msDJd28l2M5l+M2Tn28AnLG1wAI= +github.com/signadot/go-sdk v0.3.8-0.20240522192013-3882db39d258/go.mod h1:LBc3zdVqtLQpXo78HN/DrMhf6PBcfyhlg/tVRrDL+sg= +github.com/signadot/go-sdk v0.3.8-0.20240524154550-b1a43ad9a2c0 h1:bK8z1qD26+MfE4rxzHno6d06NzCqEx4JHrVfiIF5ZGc= +github.com/signadot/go-sdk v0.3.8-0.20240524154550-b1a43ad9a2c0/go.mod h1:LBc3zdVqtLQpXo78HN/DrMhf6PBcfyhlg/tVRrDL+sg= +github.com/signadot/go-sdk v0.3.8-0.20240529180930-5352d0cabf47 h1:av5aTGgRth05JoiRZwua5TPs0o9cD6OUHmhKrg8fN6A= +github.com/signadot/go-sdk v0.3.8-0.20240529180930-5352d0cabf47/go.mod h1:LBc3zdVqtLQpXo78HN/DrMhf6PBcfyhlg/tVRrDL+sg= +github.com/signadot/go-sdk v0.3.8-0.20240529233641-955e0237e964 h1:S8mXSuI/JcNTkaoXL2L0HDLGgakKffmMMswmZwrLSmM= +github.com/signadot/go-sdk v0.3.8-0.20240529233641-955e0237e964/go.mod h1:LBc3zdVqtLQpXo78HN/DrMhf6PBcfyhlg/tVRrDL+sg= github.com/signadot/libconnect v0.1.1-0.20240306100356-4c865b888453 h1:omG9Iuz5vO0wNvpX/o1sAu+yuHnjHp6okvV9dDRCcd4= github.com/signadot/libconnect v0.1.1-0.20240306100356-4c865b888453/go.mod h1:hS/87oYNXxPg5+sSQuHnQgc8q1xEsBIExnbLEeC46+8= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= @@ -355,12 +371,18 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +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/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= +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.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +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.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -418,6 +440,7 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -693,6 +716,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= +gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/command/artifact/command.go b/internal/command/artifact/command.go new file mode 100644 index 0000000..7edb915 --- /dev/null +++ b/internal/command/artifact/command.go @@ -0,0 +1,22 @@ +package artifact + +import ( + "github.com/signadot/cli/internal/config" + "github.com/spf13/cobra" +) + +func New(api *config.API) *cobra.Command { + cfg := &config.Artifact{API: api} + + cmd := &cobra.Command{ + Use: "artifact", + Short: "List and download artifacts", + } + + // Subcommands + cmd.AddCommand( + newDownload(cfg), + ) + + return cmd +} diff --git a/internal/command/artifact/download.go b/internal/command/artifact/download.go new file mode 100644 index 0000000..fcfb488 --- /dev/null +++ b/internal/command/artifact/download.go @@ -0,0 +1,88 @@ +package artifact + +import ( + "fmt" + "github.com/go-openapi/runtime" + "github.com/signadot/cli/internal/config" + "github.com/signadot/go-sdk/client" + "github.com/signadot/go-sdk/client/artifacts" + "github.com/spf13/cobra" + "io" + "os" + "path" + "strings" +) + +func newDownload(artifact *config.Artifact) *cobra.Command { + cfg := &config.ArtifactDownload{Artifact: artifact} + + cmd := &cobra.Command{ + Use: "download PATH", + Short: "Download job", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return download(cfg, cmd.OutOrStdout(), args[0]) + }, + } + + cfg.AddFlags(cmd) + + return cmd +} + +func download(cfg *config.ArtifactDownload, out io.Writer, artifactPath string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + + outputFilename := getOutputFilename(cfg, artifactPath) + f, err := os.Create(outputFilename) + if err != nil { + return err + } + + /* + If path starts with @ means is system based, otherwise user + */ + space := "user" + if strings.HasPrefix(artifactPath, "@") { + space = "system" + artifactPath = strings.TrimPrefix(artifactPath, "@") + } + + params := artifacts. + NewDownloadJobAttemptArtifactParams(). + WithOrgName(cfg.Org). + WithJobName(cfg.Job). + WithJobAttempt(0). + WithPath(artifactPath). + WithSpace(&space) + + err = cfg.APIClientWithCustomTransport(cfg.OverrideTransportClientConsumers(map[string]runtime.Consumer{ + runtime.TextMime: runtime.ByteStreamConsumer(), + }), + func(c *client.SignadotAPI) error { + _, _, err = c.Artifacts.DownloadJobAttemptArtifact(params, nil, f) + if err != nil { + return err + } + + fmt.Fprintf(out, "File saved successfully at %s\n", outputFilename) + + return nil + }) + + if err != nil { + return err + } + + return nil +} + +func getOutputFilename(cfg *config.ArtifactDownload, artifactPath string) string { + if len(cfg.OutputFile) != 0 { + return cfg.OutputFile + } + + return path.Base(artifactPath) +} diff --git a/internal/command/command.go b/internal/command/command.go index 54bbec4..a81730d 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -2,10 +2,14 @@ package command import ( "fmt" + "github.com/signadot/cli/internal/command/logs" "github.com/signadot/cli/internal/buildinfo" + "github.com/signadot/cli/internal/command/artifact" "github.com/signadot/cli/internal/command/bug" "github.com/signadot/cli/internal/command/cluster" + "github.com/signadot/cli/internal/command/jobrunnergroup" + "github.com/signadot/cli/internal/command/jobs" "github.com/signadot/cli/internal/command/local" "github.com/signadot/cli/internal/command/locald" "github.com/signadot/cli/internal/command/resourceplugin" @@ -39,6 +43,10 @@ func New() *cobra.Command { local.New(cfg), locald.New(cfg), bug.New(cfg), + jobrunnergroup.New(cfg), + jobs.New(cfg), + artifact.New(cfg), + logs.New(cfg), ) return cmd diff --git a/internal/command/jobrunnergroup/apply.go b/internal/command/jobrunnergroup/apply.go new file mode 100644 index 0000000..8a747d3 --- /dev/null +++ b/internal/command/jobrunnergroup/apply.go @@ -0,0 +1,70 @@ +package jobrunnergroup + +import ( + "errors" + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + runnergroups "github.com/signadot/go-sdk/client/runner_groups" + "github.com/signadot/go-sdk/models" + "github.com/spf13/cobra" +) + +func newApply(jobrunnergroup *config.JobRunnerGroup) *cobra.Command { + cfg := &config.JobRunnerGroupApply{JobRunnerGroup: jobrunnergroup} + + cmd := &cobra.Command{ + Use: "apply -f FILENAME [ --set var1=val1 --set var2=val2 ... ]", + Short: "Create or update a jobrunnergroup with variable expansion", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return apply(cfg, cmd.OutOrStdout(), cmd.ErrOrStderr(), args) + }, + } + + cfg.AddFlags(cmd) + + return cmd +} + +func apply(cfg *config.JobRunnerGroupApply, out, log io.Writer, _ []string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + if cfg.Filename == "" { + return errors.New("must specify jobrunnergroup request file with '-f' flag") + } + req, err := loadRunnerGroup(cfg.Filename, cfg.TemplateVals, false /*forDelete */) + if err != nil { + return err + } + + params := runnergroups.NewApplyRunnergroupParams(). + WithOrgName(cfg.Org). + WithRunnergroupName(req.Name).WithData(req) + + result, err := cfg.Client.RunnerGroups.ApplyRunnergroup(params, nil) + if err != nil { + return err + } + resp := result.Payload + + fmt.Fprintf(log, "Created runner %q (%q)\n\n", req.Name, cfg.RunnerGroupDashboardUrl(req.Name)) + + return writeOutput(cfg, out, resp) +} + +func writeOutput(cfg *config.JobRunnerGroupApply, out io.Writer, resp *models.JobRunnerGroup) error { + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return nil + case config.OutputFormatJSON: + return print.RawJSON(out, resp) + case config.OutputFormatYAML: + return print.RawYAML(out, resp) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/jobrunnergroup/command.go b/internal/command/jobrunnergroup/command.go new file mode 100644 index 0000000..31a282f --- /dev/null +++ b/internal/command/jobrunnergroup/command.go @@ -0,0 +1,26 @@ +package jobrunnergroup + +import ( + "github.com/signadot/cli/internal/config" + "github.com/spf13/cobra" +) + +func New(api *config.API) *cobra.Command { + cfg := &config.JobRunnerGroup{API: api} + + cmd := &cobra.Command{ + Use: "jobrunnergroup", + Short: "Inspect and manipulate jobrunnergroup", + Aliases: []string{"jrg"}, + } + + // Subcommands + cmd.AddCommand( + newGet(cfg), + newList(cfg), + newApply(cfg), + newDelete(cfg), + ) + + return cmd +} diff --git a/internal/command/jobrunnergroup/delete.go b/internal/command/jobrunnergroup/delete.go new file mode 100644 index 0000000..1e179d9 --- /dev/null +++ b/internal/command/jobrunnergroup/delete.go @@ -0,0 +1,71 @@ +package jobrunnergroup + +import ( + "errors" + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + runnergroups "github.com/signadot/go-sdk/client/runner_groups" + "github.com/spf13/cobra" +) + +func newDelete(jobrunnergroup *config.JobRunnerGroup) *cobra.Command { + cfg := &config.JobRunnerGroupDelete{JobRunnerGroup: jobrunnergroup} + + cmd := &cobra.Command{ + Use: "delete { NAME | -f FILENAME [ --set var1=val1 --set var2=val2 ... ] }", + Short: "Delete jobrunnergroup", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return rgDelete(cfg, cmd.ErrOrStderr(), args) + }, + } + + cfg.AddFlags(cmd) + + return cmd +} + +func rgDelete(cfg *config.JobRunnerGroupDelete, log io.Writer, args []string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + // Get the name either from a file or from the command line. + var name string + if cfg.Filename == "" { + if len(args) == 0 { + return errors.New("must specify filename (-f) or jobrunnergroup name") + } + if len(cfg.TemplateVals) != 0 { + return errors.New("must specify filename (-f) to use --set") + } + name = args[0] + } else { + if len(args) != 0 { + return errors.New("must not provide args when filename (-f) specified") + } + rg, err := loadRunnerGroup(cfg.Filename, cfg.TemplateVals, true /* forDelete */) + if err != nil { + return err + } + name = rg.Name + } + + if name == "" { + return errors.New("jobrunnergroup name is required") + } + + // Delete the jobrunnergroup. + params := runnergroups.NewDeleteRunnergroupParams(). + WithOrgName(cfg.Org). + WithRunnergroupName(name) + _, err := cfg.Client.RunnerGroups.DeleteRunnergroup(params, nil) + if err != nil { + return err + } + + fmt.Fprintf(log, "Deleted jobrunnergroup %q.\n\n", name) + + return nil +} diff --git a/internal/command/jobrunnergroup/get.go b/internal/command/jobrunnergroup/get.go new file mode 100644 index 0000000..0698f04 --- /dev/null +++ b/internal/command/jobrunnergroup/get.go @@ -0,0 +1,48 @@ +package jobrunnergroup + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + runnergroups "github.com/signadot/go-sdk/client/runner_groups" + "github.com/spf13/cobra" +) + +func newGet(jobrunnergroup *config.JobRunnerGroup) *cobra.Command { + cfg := &config.JobRunnerGroupGet{JobRunnerGroup: jobrunnergroup} + + cmd := &cobra.Command{ + Use: "get NAME", + Short: "Get jobrunnergroup", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return get(cfg, cmd.OutOrStdout(), args[0]) + }, + } + + return cmd +} + +func get(cfg *config.JobRunnerGroupGet, out io.Writer, name string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + params := runnergroups.NewGetRunnergroupParams().WithOrgName(cfg.Org).WithRunnergroupName(name) + resp, err := cfg.Client.RunnerGroups.GetRunnergroup(params, nil) + if err != nil { + return err + } + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printRunnerGroupDetails(cfg.JobRunnerGroup, out, resp.Payload) + case config.OutputFormatJSON: + return print.RawJSON(out, resp.Payload) + case config.OutputFormatYAML: + return print.RawYAML(out, resp.Payload) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/jobrunnergroup/list.go b/internal/command/jobrunnergroup/list.go new file mode 100644 index 0000000..66dd733 --- /dev/null +++ b/internal/command/jobrunnergroup/list.go @@ -0,0 +1,48 @@ +package jobrunnergroup + +import ( + "fmt" + "io" + + runnergroups "github.com/signadot/go-sdk/client/runner_groups" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + "github.com/spf13/cobra" +) + +func newList(jobrunnergroup *config.JobRunnerGroup) *cobra.Command { + cfg := &config.JobRunnerGroupList{JobRunnerGroup: jobrunnergroup} + + cmd := &cobra.Command{ + Use: "list", + Short: "List runnergroups", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return list(cfg, cmd.OutOrStdout()) + }, + } + + return cmd +} + +func list(cfg *config.JobRunnerGroupList, out io.Writer) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + resp, err := cfg.Client.RunnerGroups.ListRunnergroup(runnergroups.NewListRunnergroupParams().WithOrgName(cfg.Org), nil) + if err != nil { + return err + } + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printRunnerGroupTable(cfg, out, resp.Payload) + case config.OutputFormatJSON: + return print.RawJSON(out, resp.Payload) + case config.OutputFormatYAML: + return print.RawYAML(out, resp.Payload) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/jobrunnergroup/printers.go b/internal/command/jobrunnergroup/printers.go new file mode 100644 index 0000000..adc5c76 --- /dev/null +++ b/internal/command/jobrunnergroup/printers.go @@ -0,0 +1,61 @@ +package jobrunnergroup + +import ( + "fmt" + "io" + "time" + + "github.com/signadot/cli/internal/utils" + "github.com/xeonx/timeago" + + "text/tabwriter" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/sdtab" + "github.com/signadot/go-sdk/models" +) + +type runnerGroupRow struct { + Name string `sdtab:"NAME"` + Cluster string `sdtab:"CLUSTER"` + Created string `sdtab:"CREATED"` + Status string `sdtab:"STATUS"` +} + +func printRunnerGroupTable(cfg *config.JobRunnerGroupList, out io.Writer, rgs []*models.JobRunnerGroup) error { + t := sdtab.New[runnerGroupRow](out) + t.AddHeader() + for _, rg := range rgs { + createdAt, err := time.Parse(time.RFC3339, rg.CreatedAt) + if err != nil { + return err + } + + t.AddRow(runnerGroupRow{ + Name: rg.Name, + Cluster: rg.Spec.Cluster, + Created: timeago.NoMax(timeago.English).Format(createdAt), + Status: readiness(rg.Status), + }) + } + return t.Flush() +} + +func printRunnerGroupDetails(cfg *config.JobRunnerGroup, out io.Writer, rg *models.JobRunnerGroup) error { + tw := tabwriter.NewWriter(out, 0, 0, 3, ' ', 0) + + fmt.Fprintf(tw, "Name:\t%s\n", rg.Name) + fmt.Fprintf(tw, "Created:\t%s\n", utils.FormatTimestamp(rg.CreatedAt)) + fmt.Fprintf(tw, "Status:\t%s\n", readiness(rg.Status)) + fmt.Fprintf(tw, "Dashboard page:\t%s\n", cfg.RunnerGroupDashboardUrl(rg.Name)) + + if err := tw.Flush(); err != nil { + return err + } + + return nil +} + +func readiness(status *models.JobRunnerGroupStatus) string { + return fmt.Sprintf("%d/%d pods ready", status.Pods.Ready, status.Pods.Ready+status.Pods.NotReady) +} diff --git a/internal/command/jobrunnergroup/subst.go b/internal/command/jobrunnergroup/subst.go new file mode 100644 index 0000000..c325ea2 --- /dev/null +++ b/internal/command/jobrunnergroup/subst.go @@ -0,0 +1,37 @@ +package jobrunnergroup + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/jsonexact" + "github.com/signadot/cli/internal/utils" + "github.com/signadot/go-sdk/models" +) + +func loadRunnerGroup(file string, tplVals config.TemplateVals, forDelete bool) (*models.JobRunnerGroup, error) { + template, err := utils.LoadUnstructuredTemplate(file, tplVals, forDelete) + if err != nil { + return nil, err + } + return unstructuredToRunnerGroup(template) +} + +func unstructuredToRunnerGroup(un any) (*models.JobRunnerGroup, error) { + name, spec, err := utils.UnstructuredToNameAndSpec(un) + if err != nil { + return nil, err + } + d, err := json.Marshal(spec) + if err != nil { + return nil, err + } + rg := &models.JobRunnerGroup{Name: name} + if err := jsonexact.Unmarshal(d, &rg.Spec); err != nil { + return nil, fmt.Errorf("couldn't parse YAML jobrunnergroup definition - %s", + strings.TrimPrefix(err.Error(), "json: ")) + } + return rg, nil +} diff --git a/internal/command/jobs/cancel.go b/internal/command/jobs/cancel.go new file mode 100644 index 0000000..0f0a7ca --- /dev/null +++ b/internal/command/jobs/cancel.go @@ -0,0 +1,46 @@ +package jobs + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/go-sdk/client/jobs" + "github.com/spf13/cobra" +) + +func newCancel(job *config.Job) *cobra.Command { + cfg := &config.JobDelete{Job: job} + + cmd := &cobra.Command{ + Use: "cancel NAME }", + Short: "Cancel job", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return jobCancel(cfg, cmd.ErrOrStderr(), args[0]) + }, + } + + cfg.AddFlags(cmd) + + return cmd +} + +func jobCancel(cfg *config.JobDelete, log io.Writer, name string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + + // Cancel the job. + params := jobs.NewCancelJobParams(). + WithOrgName(cfg.Org). + WithJobName(name) + _, err := cfg.Client.Jobs.CancelJob(params, nil) + if err != nil { + return err + } + + fmt.Fprintf(log, "Job %q canceled.\n\n", name) + + return nil +} diff --git a/internal/command/jobs/command.go b/internal/command/jobs/command.go new file mode 100644 index 0000000..c1c272c --- /dev/null +++ b/internal/command/jobs/command.go @@ -0,0 +1,25 @@ +package jobs + +import ( + "github.com/signadot/cli/internal/config" + "github.com/spf13/cobra" +) + +func New(api *config.API) *cobra.Command { + cfg := &config.Job{API: api} + + cmd := &cobra.Command{ + Use: "job", + Short: "Inspect and manipulate jobs", + } + + // Subcommands + cmd.AddCommand( + newGet(cfg), + newList(cfg), + newSubmit(cfg), + newCancel(cfg), + ) + + return cmd +} diff --git a/internal/command/jobs/get.go b/internal/command/jobs/get.go new file mode 100644 index 0000000..eee44ac --- /dev/null +++ b/internal/command/jobs/get.go @@ -0,0 +1,48 @@ +package jobs + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + "github.com/signadot/go-sdk/client/jobs" + "github.com/spf13/cobra" +) + +func newGet(job *config.Job) *cobra.Command { + cfg := &config.JobGet{Job: job} + + cmd := &cobra.Command{ + Use: "get NAME", + Short: "Get job", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return get(cfg, cmd.OutOrStdout(), args[0]) + }, + } + + return cmd +} + +func get(cfg *config.JobGet, out io.Writer, name string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + params := jobs.NewGetJobParams().WithOrgName(cfg.Org).WithJobName(name) + resp, err := cfg.Client.Jobs.GetJob(params, nil) + if err != nil { + return err + } + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printJobDetails(cfg.Job, out, resp.Payload) + case config.OutputFormatJSON: + return print.RawJSON(out, resp.Payload) + case config.OutputFormatYAML: + return print.RawYAML(out, resp.Payload) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/jobs/list.go b/internal/command/jobs/list.go new file mode 100644 index 0000000..95d00d2 --- /dev/null +++ b/internal/command/jobs/list.go @@ -0,0 +1,49 @@ +package jobs + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + "github.com/signadot/go-sdk/client/jobs" + "github.com/spf13/cobra" +) + +func newList(job *config.Job) *cobra.Command { + cfg := &config.JobList{Job: job} + + cmd := &cobra.Command{ + Use: "list", + Short: "List jobs", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return list(cfg, cmd.OutOrStdout()) + }, + } + + cfg.AddFlags(cmd) + + return cmd +} + +func list(cfg *config.JobList, out io.Writer) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + resp, err := cfg.Client.Jobs.ListJobs(jobs.NewListJobsParams().WithOrgName(cfg.Org), nil) + if err != nil { + return err + } + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printJobTable(cfg, out, resp.Payload) + case config.OutputFormatJSON: + return print.RawJSON(out, resp.Payload) + case config.OutputFormatYAML: + return print.RawYAML(out, resp.Payload) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/jobs/printers.go b/internal/command/jobs/printers.go new file mode 100644 index 0000000..800f2ba --- /dev/null +++ b/internal/command/jobs/printers.go @@ -0,0 +1,254 @@ +package jobs + +import ( + "fmt" + "io" + "sort" + "text/tabwriter" + "time" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/sdtab" + "github.com/signadot/go-sdk/client/artifacts" + "github.com/signadot/go-sdk/models" + "github.com/xeonx/timeago" +) + +const MaxJobListing = 20 + +type jobRow struct { + Name string `sdtab:"NAME"` + Environment string `sdtab:"ENVIRONMENT"` + CreatedAt string `sdtab:"CREATED AT"` + StartedAt string `sdtab:"STARTED AT"` + Duration string `sdtab:"DURATION"` + Status string `sdtab:"STATUS"` +} + +func printJobTable(cfg *config.JobList, out io.Writer, jobs []*models.Job) error { + t := sdtab.New[jobRow](out) + t.AddHeader() + + sort.Slice(jobs, func(i, j int) bool { + if jobs[i].Status.Attempts[0].Phase == "queued" { + return true + } + + if jobs[j].Status.Attempts[0].Phase == "queued" { + return false + } + + t1, err1 := time.Parse(time.RFC3339, jobs[i].CreatedAt) + t2, err2 := time.Parse(time.RFC3339, jobs[j].CreatedAt) + if err1 != nil || err2 != nil { + return false + } + + return t2.Before(t1) + }) + + counter := 0 + for _, job := range jobs { + if counter == MaxJobListing { + break + } + + switch { + case cfg.ShowAll: + case !cfg.ShowAll && job.Status.Phase != "completed" && job.Status.Phase != "canceled": + default: + continue + } + + counter += 1 + + createdAt, duration := getAttemptCreatedAtAndDuration(job) + + environment := "" + routingContext := job.Spec.RoutingContext + switch { + case routingContext == nil: + case len(routingContext.Sandbox) > 0: + environment = fmt.Sprintf("sandbox=%s", routingContext.Sandbox) + case len(routingContext.Routegroup) > 0: + environment += fmt.Sprintf("routegroup=%s", routingContext.Routegroup) + } + + t.AddRow(jobRow{ + Name: job.Name, + Environment: environment, + StartedAt: createdAt, + Duration: duration, + Status: job.Status.Phase, + CreatedAt: getCreatedAt(job), + }) + } + return t.Flush() +} + +func printJobDetails(cfg *config.Job, out io.Writer, job *models.Job) error { + tw := tabwriter.NewWriter(out, 0, 0, 3, ' ', 0) + + createdAt, duration := getAttemptCreatedAtAndDuration(job) + + fmt.Fprintf(tw, "Job Name:\t%s\n", job.Name) + fmt.Fprintf(tw, "Job Runner Group:\t%s\n", job.Spec.RunnerGroup) + fmt.Fprintf(tw, "Status:\t%s\n", job.Status.Phase) + fmt.Fprintf(tw, "Environment:\t%s\n", getJobEnvironment(job)) + fmt.Fprintf(tw, "Created At:\t%s\n", getCreatedAt(job)) + + if len(createdAt) != 0 { + fmt.Fprintf(tw, "Started At:\t%s\n", createdAt) + } + + if len(duration) != 0 { + fmt.Fprintf(tw, "Duration:\t%s\n", duration) + } + + fmt.Fprintf(tw, "Dashboard URL:\t%s\n", cfg.JobDashboardUrl(job.Name)) + + if err := printArtifacts(cfg, tw, job); err != nil { + return err + } + + if err := tw.Flush(); err != nil { + return err + } + + return nil +} + +func getCreatedAt(job *models.Job) string { + createdAt := job.CreatedAt + if len(createdAt) == 0 { + return "" + } + + t, err := time.Parse(time.RFC3339, createdAt) + if err != nil { + return "" + } + + return timeago.NoMax(timeago.English).Format(t) +} + +func getAttemptCreatedAtAndDuration(job *models.Job) (createdAtStr string, durationStr string) { + var createdAt *time.Time + + if len(job.Status.Attempts) == 0 { + return "", "" + } + + attempt := job.Status.Attempts[0] + + if attempt.Phase == "queued" { + return "", "" + } + + createdAtRaw := attempt.CreatedAt + if len(createdAtRaw) != 0 { + t, err := time.Parse(time.RFC3339, createdAtRaw) + if err != nil { + return "", "" + } + + createdAt = &t + createdAtStr = timeago.NoMax(timeago.English).Format(t) + } + + finishedAtRaw := attempt.FinishedAt + if createdAt != nil && len(finishedAtRaw) != 0 { + finishedAt, err := time.Parse(time.RFC3339, finishedAtRaw) + if err != nil { + return "", "" + } + + durationTime := finishedAt.Sub(*createdAt) + durationStr = durationTime.String() + } + + return createdAtStr, durationStr +} + +func getJobEnvironment(job *models.Job) string { + routingContext := job.Spec.RoutingContext + + if routingContext == nil { + return "BASELINE" + } + + if len(routingContext.Sandbox) > 0 { + return fmt.Sprintf("%s (SANDBOX)", routingContext.Sandbox) + } + + return fmt.Sprintf("%s (ROUTEGROUP)", routingContext.Routegroup) +} + +func getArtifacts(cfg *config.Job, job *models.Job) ([]*models.JobArtifact, error) { + params := artifacts.NewListJobAttemptArtifactsParams(). + WithOrgName(cfg.Org). + WithJobAttempt(job.Status.Attempts[0].ID). + WithJobName(job.Name) + + resp, err := cfg.Client.Artifacts.ListJobAttemptArtifacts(params, nil) + if err != nil { + return []*models.JobArtifact{}, nil + } + + return resp.Payload, nil +} + +type jobArtifactRow struct { + Path string `sdtab:"PATH"` + Size string `sdtab:"SIZE"` +} + +func printArtifacts(cfg *config.Job, out io.Writer, job *models.Job) error { + artifactsList, err := getArtifacts(cfg, job) + if err != nil { + return err + } + + fmt.Fprintf(out, "\nArtifacts\n") + + if len(artifactsList) == 0 { + fmt.Fprintln(out, "No artifacts") + return nil + } + + t := sdtab.New[jobArtifactRow](out) + t.AddHeader() + + excludeFiles := map[string]bool{"stderr.index": true, "stdout.index": true} + for _, artifact := range artifactsList { + path := artifact.Path + + if _, ok := excludeFiles[path]; ok { + continue + } + + if artifact.Space == "system" { + path = "@" + path + } + + t.AddRow(jobArtifactRow{ + Path: path, + Size: byteCountSI(artifact.Size), + }) + } + return t.Flush() +} + +func byteCountSI(b int64) string { + const unit = 1000 + if b < unit { + return fmt.Sprintf("%d B", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cB", + float64(b)/float64(div), "kMGTPE"[exp]) +} diff --git a/internal/command/jobs/submit.go b/internal/command/jobs/submit.go new file mode 100644 index 0000000..5d5d8d9 --- /dev/null +++ b/internal/command/jobs/submit.go @@ -0,0 +1,71 @@ +package jobs + +import ( + "errors" + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + "github.com/signadot/go-sdk/client/jobs" + "github.com/signadot/go-sdk/models" + "github.com/spf13/cobra" +) + +func newSubmit(job *config.Job) *cobra.Command { + cfg := &config.JobSubmit{Job: job} + + cmd := &cobra.Command{ + Use: "submit -f FILENAME [ --set var1=val1 --set var2=val2 ... ]", + Short: "Create or update a job with variable expansion", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return submit(cfg, cmd.OutOrStdout(), cmd.ErrOrStderr(), args) + }, + } + + cfg.AddFlags(cmd) + + return cmd +} + +func submit(cfg *config.JobSubmit, out, log io.Writer, args []string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + if cfg.Filename == "" { + return errors.New("must specify job request file with '-f' flag") + } + req, err := loadJob(cfg.Filename, cfg.TemplateVals, false /*forDelete */) + if err != nil { + return err + } + + params := jobs.NewCreateJobParams(). + WithOrgName(cfg.Org).WithData(req) + result, err := cfg.Client.Jobs.CreateJob(params, nil) + if err != nil { + return err + } + resp := result.Payload + + fmt.Fprintf(log, "Job %s queued on Job Runner Group: %s\n", resp.Name, resp.Spec.RunnerGroup) + + return writeOutput(cfg, out, resp) +} + +func writeOutput(cfg *config.JobSubmit, out io.Writer, resp *models.Job) error { + switch cfg.OutputFormat { + case config.OutputFormatDefault: + // Print info on how to access the job. + fmt.Fprintf(out, "\nDashboard page: %v\n\n", cfg.JobDashboardUrl(resp.Name)) + + return nil + case config.OutputFormatJSON: + return print.RawJSON(out, resp) + case config.OutputFormatYAML: + return print.RawYAML(out, resp) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/jobs/subst.go b/internal/command/jobs/subst.go new file mode 100644 index 0000000..a215d7f --- /dev/null +++ b/internal/command/jobs/subst.go @@ -0,0 +1,41 @@ +package jobs + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/jsonexact" + "github.com/signadot/cli/internal/utils" + "github.com/signadot/go-sdk/models" +) + +func loadJob(file string, tplVals config.TemplateVals, forDelete bool) (*models.Job, error) { + template, err := utils.LoadUnstructuredTemplate(file, tplVals, forDelete) + if err != nil { + return nil, err + } + + return unstructuredToJob(template) +} + +func unstructuredToJob(un any) (*models.Job, error) { + raw, ok := un.(map[string]any) + if !ok { + return nil, errors.New("missing spec field") + } + spec := raw["spec"] + + d, err := json.Marshal(spec) + if err != nil { + return nil, err + } + rg := &models.Job{} + if err := jsonexact.Unmarshal(d, &rg.Spec); err != nil { + return nil, fmt.Errorf("couldn't parse YAML job definition - %s", + strings.TrimPrefix(err.Error(), "json: ")) + } + return rg, nil +} diff --git a/internal/command/logs/command.go b/internal/command/logs/command.go new file mode 100644 index 0000000..25ea64f --- /dev/null +++ b/internal/command/logs/command.go @@ -0,0 +1,88 @@ +package logs + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/r3labs/sse/v2" + "github.com/signadot/cli/internal/config" + "github.com/spf13/cobra" + "io" + "net/url" + "path" + "strings" +) + +func New(api *config.API) *cobra.Command { + cfg := &config.Logs{API: api} + + cmd := &cobra.Command{ + Use: "logs", + Short: "Inspect and manipulate artifact", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return list(cfg, cmd.OutOrStdout()) + }, + } + + cfg.AddFlags(cmd) + + return cmd +} + +func list(cfg *config.Logs, out io.Writer) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + + u := url.URL{ + Scheme: "https", + Path: path.Join(strings.TrimPrefix(cfg.APIURL, "https://"), "api/v2/orgs", cfg.Org, "/jobs", cfg.Job, "/attempts/0/logs/stream"), + RawQuery: "type=" + cfg.Stream, + } + + events := make(chan *sse.Event) + + client := sse.NewClient(u.String()) + client.Headers = map[string]string{ + "Signadot-Api-Key": cfg.ApiKey, + } + + err := client.SubscribeChan("", events) + if err != nil { + return err + } + + for { + select { + case event := <-events: + type Log struct { + Message string `json:"message"` + } + + switch string(event.Event) { + case "message": + var log Log + if err := json.Unmarshal(event.Data, &log); err != nil { + return err + } + + fmt.Print(log.Message) + case "error": + return errors.New(string(event.Data)) + case "signal": + + switch string(event.Data) { + case "EOF": + fmt.Println() + return nil + case "RESTART": + fmt.Println("\n\n-----------") + default: + return nil + } + } + } + } + +} diff --git a/internal/command/resourceplugin/printers.go b/internal/command/resourceplugin/printers.go index 534e33c..39ec41e 100644 --- a/internal/command/resourceplugin/printers.go +++ b/internal/command/resourceplugin/printers.go @@ -2,12 +2,13 @@ package resourceplugin import ( "fmt" + "io" + "text/tabwriter" + "github.com/signadot/cli/internal/config" "github.com/signadot/cli/internal/sdtab" "github.com/signadot/cli/internal/utils" "github.com/signadot/go-sdk/models" - "io" - "text/tabwriter" ) type resourcePluginRow struct { diff --git a/internal/command/routegroup/printers.go b/internal/command/routegroup/printers.go index 1242b0d..621143c 100644 --- a/internal/command/routegroup/printers.go +++ b/internal/command/routegroup/printers.go @@ -2,13 +2,14 @@ package routegroup import ( "fmt" - "github.com/signadot/cli/internal/utils" - "github.com/signadot/go-sdk/client/sandboxes" - "github.com/xeonx/timeago" "io" "text/tabwriter" "time" + "github.com/signadot/cli/internal/utils" + "github.com/signadot/go-sdk/client/sandboxes" + "github.com/xeonx/timeago" + "github.com/signadot/cli/internal/config" "github.com/signadot/cli/internal/sdtab" "github.com/signadot/go-sdk/models" diff --git a/internal/command/sandbox/printers.go b/internal/command/sandbox/printers.go index daf8eef..07cf946 100644 --- a/internal/command/sandbox/printers.go +++ b/internal/command/sandbox/printers.go @@ -2,14 +2,15 @@ package sandbox import ( "fmt" - "github.com/docker/go-units" - "github.com/signadot/cli/internal/utils" - "github.com/xeonx/timeago" "io" "strconv" "text/tabwriter" "time" + "github.com/docker/go-units" + "github.com/signadot/cli/internal/utils" + "github.com/xeonx/timeago" + "github.com/signadot/cli/internal/config" "github.com/signadot/cli/internal/sdtab" "github.com/signadot/go-sdk/models" diff --git a/internal/config/api.go b/internal/config/api.go index 006a51c..3a2d34c 100644 --- a/internal/config/api.go +++ b/internal/config/api.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" "fmt" - + "github.com/go-openapi/runtime" "github.com/signadot/cli/internal/buildinfo" "github.com/signadot/go-sdk/client" "github.com/signadot/go-sdk/transport" @@ -16,12 +16,16 @@ type API struct { Root // Config file values - Org string - MaskedAPIKey string - APIURL string + Org string + MaskedAPIKey string + APIURL string + ArtifactsAPIURL string // Runtime values Client *client.SignadotAPI `json:"-"` + + ApiKey string + UserAgent string } // for error reporting, we select the config that @@ -52,47 +56,81 @@ func (a *API) marshal(marshaller func(interface{}) ([]byte, error)) ([]byte, err return marshaller(t) } -func (a *API) InitAPIConfig() error { +func (a *API) init() error { apiKey := viper.GetString("api_key") if apiKey == "" { return errors.New("Signadot API key must be specified through either the SIGNADOT_API_KEY env var or the 'api_key' field in ~/.signadot/config.yaml") } else { a.MaskedAPIKey = apiKey[:6] + "..." } + a.ApiKey = apiKey a.Org = viper.GetString("org") if a.Org == "" { return errors.New("Signadot Org name must be specified through either the SIGNADOT_ORG env var or the 'org' field in ~/.signadot/config.yaml") } - return a.InitAPITransport(apiKey) -} - -func (a *API) InitAPITransport(apiKey string) error { - // Allow API URL to be overridden (e.g. for talking to dev/staging). if apiURL := viper.GetString("api_url"); apiURL != "" { a.APIURL = apiURL } else { a.APIURL = "https://api.signadot.com" } + // Allow defining a custom URL for artifacts (useful for local development). // Empty means using the API URL from above for accessing artifacts. - artifactsAPIURL := viper.GetString("artifacts_api_url") + a.ArtifactsAPIURL = viper.GetString("artifacts_api_url") - // init API transport - transport, err := transport.InitAPITransport(&transport.APIConfig{ - APIKey: apiKey, + a.UserAgent = fmt.Sprintf("signadot-cli:%s", buildinfo.Version) + return nil +} + +func (a *API) InitAPIConfig() error { + + if err := a.init(); err != nil { + return err + } + + return a.InitAPITransport() +} + +func (a *API) getBaseTransport() *transport.APIConfig { + return &transport.APIConfig{ + APIKey: a.ApiKey, APIURL: a.APIURL, - ArtifactsAPIURL: artifactsAPIURL, - UserAgent: fmt.Sprintf("signadot-cli:%s", buildinfo.Version), + ArtifactsAPIURL: a.ArtifactsAPIURL, + UserAgent: a.UserAgent, Debug: a.Debug, - }) + } +} + +func (a *API) InitAPITransport() error { + // init API transport + t, err := transport.InitAPITransport(a.getBaseTransport()) if err != nil { return err } // create an API client - a.Client = client.New(transport, nil) + a.Client = client.New(t, nil) return nil } + +func (a *API) APIClientWithCustomTransport(conf *transport.APIConfig, execute func(client *client.SignadotAPI) error) error { + if err := a.init(); err != nil { + return nil + } + + t, err := transport.InitAPITransport(conf) + if err != nil { + return err + } + + return execute(client.New(t, nil)) +} + +func (a *API) OverrideTransportClientConsumers(consumers map[string]runtime.Consumer) *transport.APIConfig { + cfg := a.getBaseTransport() + cfg.Consumers = consumers + return cfg +} diff --git a/internal/config/artifacts.go b/internal/config/artifacts.go new file mode 100644 index 0000000..5e21165 --- /dev/null +++ b/internal/config/artifacts.go @@ -0,0 +1,28 @@ +package config + +import ( + "github.com/spf13/cobra" +) + +type Artifact struct { + *API +} + +type ArtifactDownload struct { + *Artifact + + // Flags + Job string + OutputFile string + + //Attempt int64 +} + +func (c *ArtifactDownload) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVarP(&c.Job, "job", "j", "", "job name where to get the attempt artifact") + cmd.MarkFlagRequired("job") + + cmd.Flags().StringVarP(&c.OutputFile, "output", "o", "", "path where the file would be downloaded") + + //cmd.Flags().Int64VarP(&c.Attempt, "attempt", "a", 0, "number of the attempt to get") +} diff --git a/internal/config/job.go b/internal/config/job.go new file mode 100644 index 0000000..f879052 --- /dev/null +++ b/internal/config/job.go @@ -0,0 +1,42 @@ +package config + +import ( + "github.com/spf13/cobra" +) + +type Job struct { + *API +} + +type JobSubmit struct { + *Job + + // Flags + Filename string + TemplateVals TemplateVals +} + +func (c *JobSubmit) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVarP(&c.Filename, "filename", "f", "", "YAML or JSON file containing the jobs creation request") + cmd.MarkFlagRequired("filename") + cmd.Flags().Var(&c.TemplateVals, "set", "--set var=val") +} + +type JobDelete struct { + *Job +} + +type JobGet struct { + *Job +} + +type JobList struct { + *Job + + // Flags + ShowAll bool +} + +func (c *JobList) AddFlags(cmd *cobra.Command) { + cmd.Flags().BoolVarP(&c.ShowAll, "all", "", false, "List all jobs") +} diff --git a/internal/config/logs.go b/internal/config/logs.go new file mode 100644 index 0000000..49f5f76 --- /dev/null +++ b/internal/config/logs.go @@ -0,0 +1,19 @@ +package config + +import ( + "github.com/spf13/cobra" +) + +type Logs struct { + *API + + Job string + Stream string +} + +func (c *Logs) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVarP(&c.Job, "job", "j", "", "job name where to get the attempt Logs") + cmd.MarkFlagRequired("job") + + cmd.Flags().StringVarP(&c.Stream, "stream", "s", "stdout", "channel where get the logs stdout or stderr") +} diff --git a/internal/config/root.go b/internal/config/root.go index da04a91..c489e20 100644 --- a/internal/config/root.go +++ b/internal/config/root.go @@ -2,12 +2,11 @@ package config import ( "fmt" - "net/url" - "path" - "github.com/signadot/cli/internal/utils/system" "github.com/spf13/cobra" "github.com/spf13/viper" + "net/url" + "path" ) type Root struct { @@ -80,3 +79,15 @@ func (c *Root) SandboxDashboardURL(id string) *url.URL { u.Path = path.Join(u.Path, "sandbox", "id", id) return &u } + +func (c *Root) RunnerGroupDashboardUrl(name string) *url.URL { + u := *c.DashboardURL + u.Path = path.Join(u.Path, "testing", "runner-groups", name) + return &u +} + +func (c *Root) JobDashboardUrl(name string) *url.URL { + u := *c.DashboardURL + u.Path = path.Join(u.Path, "testing", "jobs", name) + return &u +} diff --git a/internal/config/runnergroup.go b/internal/config/runnergroup.go new file mode 100644 index 0000000..8bcac1c --- /dev/null +++ b/internal/config/runnergroup.go @@ -0,0 +1,44 @@ +package config + +import ( + "github.com/spf13/cobra" +) + +type JobRunnerGroup struct { + *API +} + +type JobRunnerGroupApply struct { + *JobRunnerGroup + + // Flags + Filename string + TemplateVals TemplateVals +} + +func (c *JobRunnerGroupApply) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVarP(&c.Filename, "filename", "f", "", "YAML or JSON file containing the jobrunnergroup creation request") + cmd.MarkFlagRequired("filename") + cmd.Flags().Var(&c.TemplateVals, "set", "--set var=val") +} + +type JobRunnerGroupDelete struct { + *JobRunnerGroup + + // Flags + Filename string + TemplateVals TemplateVals +} + +func (c *JobRunnerGroupDelete) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVarP(&c.Filename, "filename", "f", "", "optional YAML or JSON file containing the original routegroup creation request") + cmd.Flags().Var(&c.TemplateVals, "set", "--set var=val") +} + +type JobRunnerGroupGet struct { + *JobRunnerGroup +} + +type JobRunnerGroupList struct { + *JobRunnerGroup +} diff --git a/internal/locald/api/common.pb.go b/internal/locald/api/common.pb.go index dedd14b..0a1c6b1 100644 --- a/internal/locald/api/common.pb.go +++ b/internal/locald/api/common.pb.go @@ -7,11 +7,12 @@ package api import ( + reflect "reflect" + sync "sync" + timestamp "github.com/golang/protobuf/ptypes/timestamp" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" ) const ( diff --git a/internal/locald/api/rootmanager/root_manager_api.pb.go b/internal/locald/api/rootmanager/root_manager_api.pb.go index 4c51e59..65ebfa8 100644 --- a/internal/locald/api/rootmanager/root_manager_api.pb.go +++ b/internal/locald/api/rootmanager/root_manager_api.pb.go @@ -7,11 +7,12 @@ package rootmanager import ( + reflect "reflect" + sync "sync" + api "github.com/signadot/cli/internal/locald/api" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" ) const ( diff --git a/internal/locald/api/rootmanager/root_manager_api_grpc.pb.go b/internal/locald/api/rootmanager/root_manager_api_grpc.pb.go index 756ed07..59caaaf 100644 --- a/internal/locald/api/rootmanager/root_manager_api_grpc.pb.go +++ b/internal/locald/api/rootmanager/root_manager_api_grpc.pb.go @@ -8,6 +8,7 @@ package rootmanager import ( context "context" + grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" diff --git a/internal/locald/api/sandboxmanager/sandbox_manager_api.pb.go b/internal/locald/api/sandboxmanager/sandbox_manager_api.pb.go index df2c3db..380d537 100644 --- a/internal/locald/api/sandboxmanager/sandbox_manager_api.pb.go +++ b/internal/locald/api/sandboxmanager/sandbox_manager_api.pb.go @@ -7,12 +7,13 @@ package sandboxmanager import ( + reflect "reflect" + sync "sync" + _struct "github.com/golang/protobuf/ptypes/struct" api "github.com/signadot/cli/internal/locald/api" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" ) const ( diff --git a/internal/locald/api/sandboxmanager/sandbox_manager_api_grpc.pb.go b/internal/locald/api/sandboxmanager/sandbox_manager_api_grpc.pb.go index a19a4d3..9bf2ec5 100644 --- a/internal/locald/api/sandboxmanager/sandbox_manager_api_grpc.pb.go +++ b/internal/locald/api/sandboxmanager/sandbox_manager_api_grpc.pb.go @@ -8,6 +8,7 @@ package sandboxmanager import ( context "context" + grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" diff --git a/internal/locald/rootmanager/sbmgr_monitor.go b/internal/locald/rootmanager/sbmgr_monitor.go index 1b41dc8..8934637 100644 --- a/internal/locald/rootmanager/sbmgr_monitor.go +++ b/internal/locald/rootmanager/sbmgr_monitor.go @@ -9,10 +9,11 @@ import ( "sync" "time" + "log/slog" + "github.com/signadot/cli/internal/config" sbmapi "github.com/signadot/cli/internal/locald/api/sandboxmanager" "github.com/signadot/libconnect/common/processes" - "log/slog" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) diff --git a/internal/locald/run.go b/internal/locald/run.go index bffbf4c..ac32870 100644 --- a/internal/locald/run.go +++ b/internal/locald/run.go @@ -5,10 +5,11 @@ import ( "fmt" "os" + "log/slog" + "github.com/signadot/cli/internal/config" "github.com/signadot/cli/internal/locald/rootmanager" sbmgr "github.com/signadot/cli/internal/locald/sandboxmanager" - "log/slog" ) func RunSandboxManager(cfg *config.LocalDaemon, log *slog.Logger, args []string) error { diff --git a/internal/utils/time.go b/internal/utils/time.go index eab51d3..5a90de9 100644 --- a/internal/utils/time.go +++ b/internal/utils/time.go @@ -2,10 +2,11 @@ package utils import ( "fmt" - "github.com/docker/go-units" - "github.com/xeonx/timeago" "strconv" "time" + + "github.com/docker/go-units" + "github.com/xeonx/timeago" ) func FormatTimestamp(in string) string {