diff --git a/2do.md b/2do.md index d221b54..3a2c28c 100644 --- a/2do.md +++ b/2do.md @@ -49,4 +49,15 @@ - [ ] `dockeroller/docker/docker.go:29` We should get Docker ENVs from user in case of not default - [ ] Add linter (golangci-linter to actions) - [ ] Pass ID to session like the way dongi works, or maybe better! +- [ ] What happens if we have a job that permanently pull images list and containers list from docker and we use that instead of using the one we already have? +- [ ] When there are multiple tags for an image, there should be a possibility to remove only one of them (like docker desktop), not all at once +----- +In Telegram, we have a few UI components, +for example, Lists and Forms +Also we have some events that we can have actions on, ++ OnCallback, OnPhoto, OnQuery, etc. ++ Simple texts (handle through scene&question management) ++ ReplyMarkup keyboards text + +We also need a session to store user state \ No newline at end of file diff --git a/Makefile b/Makefile index cbd1771..f29a9cd 100644 --- a/Makefile +++ b/Makefile @@ -16,4 +16,7 @@ sample-docker: # Run docker with sample start command gen: go generate ./... + +start: + go run main.go start \ No newline at end of file diff --git a/cmd/start.go b/cmd/start.go index 8f89281..bd5740b 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -1,14 +1,17 @@ package cmd import ( + "net/http" "os" "time" "github.com/arshamalh/dockeroller/docker" + "github.com/arshamalh/dockeroller/entities" "github.com/arshamalh/dockeroller/log" "github.com/arshamalh/dockeroller/session" tpkg "github.com/arshamalh/dockeroller/telegram" "github.com/arshamalh/dockeroller/telegram/handlers" + "github.com/arshamalh/dockeroller/telegram/middlewares" "github.com/joho/godotenv" "github.com/spf13/cobra" "gopkg.in/telebot.v3" @@ -54,8 +57,10 @@ func start(token string, whitelistedIDs []int64) { func startTelegram(docker docker.Docker, token string, whitelistedIDs []int64) { bot, err := telebot.NewBot(telebot.Settings{ - Token: token, - Poller: &telebot.LongPoller{Timeout: 10 * time.Second}, + Token: token, + Poller: &telebot.LongPoller{Timeout: 10 * time.Second}, + ParseMode: telebot.ModeMarkdownV2, + Client: &http.Client{Timeout: entities.CLIENT_TIMEOUT}, }) if err != nil { log.Gl.Error(err.Error()) @@ -65,7 +70,7 @@ func startTelegram(docker docker.Docker, token string, whitelistedIDs []int64) { // Middlewares bot.Use(middleware.Whitelist(whitelistedIDs...)) // TODO: Disabled logger middleware for now. - // bot.Use(LoggerMiddleware(log.Gl)) + bot.Use(middlewares.LoggerMiddleware(log.Gl)) if err := bot.SetCommands(tpkg.Commands); err != nil { log.Gl.Error(err.Error()) } diff --git a/docker/containers.go b/docker/containers.go index aa8cae9..a478510 100644 --- a/docker/containers.go +++ b/docker/containers.go @@ -5,17 +5,22 @@ import ( "io" "github.com/arshamalh/dockeroller/entities" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" ) -func (d *docker) ContainersList(ctx context.Context, filters filters.Args) (containers []*entities.Container) { - raw_containers, _ := d.cli.ContainerList(ctx, - types.ContainerListOptions{ +func (d *docker) ContainersList(ctx context.Context, filters filters.Args) ([]*entities.Container, error) { + raw_containers, err := d.cli.ContainerList(ctx, + container.ListOptions{ All: true, Filters: filters, }, ) + if err != nil { + return nil, err + } + + containers := make([]*entities.Container, 0) for _, raw_cont := range raw_containers { containers = append(containers, &entities.Container{ ID: raw_cont.ID, @@ -25,11 +30,11 @@ func (d *docker) ContainersList(ctx context.Context, filters filters.Args) (cont State: entities.ContainerState(raw_cont.State), }) } - return + return containers, nil } -func (d *docker) GetContainer(containerID string) (*entities.Container, error) { - container, err := d.cli.ContainerInspect(context.TODO(), containerID) +func (d *docker) GetContainer(ctx context.Context, containerID string) (*entities.Container, error) { + container, err := d.cli.ContainerInspect(ctx, containerID) if err != nil { return nil, err } @@ -42,14 +47,14 @@ func (d *docker) GetContainer(containerID string) (*entities.Container, error) { }, nil } -func (d *docker) ContainerStats(containerID string) (io.ReadCloser, error) { - stats, err := d.cli.ContainerStats(context.TODO(), containerID, true) +func (d *docker) ContainerStats(ctx context.Context, containerID string) (io.ReadCloser, error) { + stats, err := d.cli.ContainerStats(ctx, containerID, true) return stats.Body, err } -func (d *docker) ContainerLogs(containerID string) (io.ReadCloser, error) { +func (d *docker) ContainerLogs(ctx context.Context, containerID string) (io.ReadCloser, error) { // TODO: Interesting options about logs are available, you can get them from user settings - return d.cli.ContainerLogs(context.TODO(), containerID, types.ContainerLogsOptions{ + return d.cli.ContainerLogs(ctx, containerID, container.LogsOptions{ ShowStdout: true, ShowStderr: true, Follow: true, @@ -57,21 +62,21 @@ func (d *docker) ContainerLogs(containerID string) (io.ReadCloser, error) { }) } -func (d *docker) ContainerStart(containerID string) error { - return d.cli.ContainerStart(context.TODO(), containerID, types.ContainerStartOptions{}) +func (d *docker) ContainerStart(ctx context.Context, containerID string) error { + return d.cli.ContainerStart(ctx, containerID, container.StartOptions{}) } -func (d *docker) ContainerStop(containerID string) error { - return d.cli.ContainerStop(context.TODO(), containerID, nil) +func (d *docker) ContainerStop(ctx context.Context, containerID string) error { + return d.cli.ContainerStop(ctx, containerID, container.StopOptions{}) } -func (d *docker) ContainerRemove(containerID string, removeForm *entities.ContainerRemoveForm) error { - return d.cli.ContainerRemove(context.TODO(), containerID, types.ContainerRemoveOptions{ +func (d *docker) ContainerRemove(ctx context.Context, containerID string, removeForm *entities.ContainerRemoveForm) error { + return d.cli.ContainerRemove(ctx, containerID, container.RemoveOptions{ RemoveVolumes: removeForm.RemoveVolumes, Force: removeForm.Force, }) } -func (d *docker) ContainerRename(containerID, newName string) error { - return d.cli.ContainerRename(context.TODO(), containerID, newName) +func (d *docker) ContainerRename(ctx context.Context, containerID, newName string) error { + return d.cli.ContainerRename(ctx, containerID, newName) } diff --git a/docker/docker.go b/docker/docker.go index e64738d..f37bbeb 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -11,22 +11,27 @@ import ( ) type Docker interface { - GetContainer(containerID string) (*entities.Container, error) - ContainersList(ctx context.Context, filters filters.Args) (containers []*entities.Container) - ContainerLogs(containerID string) (io.ReadCloser, error) - ContainerStats(containerID string) (io.ReadCloser, error) - ContainerStart(containerID string) error - ContainerStop(containerID string) error - ContainerRemove(containerID string, removeForm *entities.ContainerRemoveForm) error - ContainerRename(containerID, newName string) error + // TODO: Add context to these functions, as they all accept context in their lower level + GetContainer(ctx context.Context, containerID string) (*entities.Container, error) + ContainersList(ctx context.Context, filters filters.Args) ([]*entities.Container, error) + // TODO: context for these one is so important as we might be able to remove quite channel by its help + ContainerLogs(ctx context.Context, containerID string) (io.ReadCloser, error) + ContainerStats(ctx context.Context, containerID string) (io.ReadCloser, error) + ContainerStart(ctx context.Context, containerID string) error + ContainerStop(ctx context.Context, containerID string) error + ContainerRemove(ctx context.Context, containerID string, removeForm *entities.ContainerRemoveForm) error + ContainerRename(ctx context.Context, containerID, newName string) error - ImagesList() []*entities.Image + ImagesList(ctx context.Context, filters filters.Args) ([]*entities.Image, error) ImageTag(ctx context.Context, imageID, newTag string) error ImageRemove(ctx context.Context, imageID string, force, pruneChildren bool) error } func New() *docker { - cli, err := client.NewClientWithOpts(client.FromEnv) + cli, err := client.NewClientWithOpts( + client.FromEnv, + client.WithAPIVersionNegotiation(), + ) if err != nil { log.Gl.Error(err.Error()) } diff --git a/docker/images.go b/docker/images.go index 4b87a92..6c10ba3 100644 --- a/docker/images.go +++ b/docker/images.go @@ -6,12 +6,17 @@ import ( "time" "github.com/arshamalh/dockeroller/entities" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/image" ) -func (d *docker) ImagesList() (images []*entities.Image) { - rawImages, _ := d.cli.ImageList(context.TODO(), types.ImageListOptions{All: true}) +func (d *docker) ImagesList(ctx context.Context, filters filters.Args) ([]*entities.Image, error) { + rawImages, err := d.cli.ImageList(context.TODO(), image.ListOptions{All: true}) + if err != nil { + return nil, err + } + + images := make([]*entities.Image, 0) for _, rawImg := range rawImages { status, containers := d.getImageStatus(context.TODO(), rawImg) images = append(images, &entities.Image{ @@ -23,7 +28,25 @@ func (d *docker) ImagesList() (images []*entities.Image) { CreatedAt: fmt.Sprint(time.Unix(rawImg.Created, 0).Format("2006-01-02 15:04:05")), }) } - return + return images, nil +} + +func (d *docker) GetImage(ctx context.Context, imageID string) (*entities.Image, error) { + image, _, err := d.cli.ImageInspectWithRaw(ctx, imageID) + if err != nil { + return nil, err + } + + return &entities.Image{ + ID: image.ID, + Size: image.Size, + CreatedAt: image.Created, + Tags: image.RepoTags, + // TODO: not sure what to do with status and usedBy as they are not working the same as ImagesList is. + // Status: entities.ImageStatus(image), + // UsedBy: , + }, nil + } func (d *docker) ImageTag(ctx context.Context, imageID, newTag string) error { @@ -32,7 +55,7 @@ func (d *docker) ImageTag(ctx context.Context, imageID, newTag string) error { func (d *docker) ImageRemove(ctx context.Context, imageID string, force, pruneChildren bool) error { _, err := d.cli.ImageRemove(ctx, imageID, - types.ImageRemoveOptions{ + image.RemoveOptions{ Force: force, PruneChildren: pruneChildren, }, @@ -42,7 +65,7 @@ func (d *docker) ImageRemove(ctx context.Context, imageID string, force, pruneCh // Returns whether an image is dangling or used by containers, // returns a list of ContainerIDs as the second argument, or nil if the Image is dangling or unused -func (d *docker) getImageStatus(ctx context.Context, image types.ImageSummary) (entities.ImageStatus, []*entities.Container) { +func (d *docker) getImageStatus(ctx context.Context, image image.Summary) (entities.ImageStatus, []*entities.Container) { if len(image.RepoTags) == 0 { return entities.ImageStatusUnUsedDangling, nil } @@ -52,7 +75,10 @@ func (d *docker) getImageStatus(ctx context.Context, image types.ImageSummary) ( return entities.ImageStatusUnUsedDangling, nil } - containers := d.ContainersList(ctx, filters.NewArgs(filters.Arg("ancestor", image.ID))) + containers, err := d.ContainersList(ctx, filters.NewArgs(filters.Arg("ancestor", image.ID))) + if err != nil { + // TODO: Handle the error, maybe we should not have this call here + } if len(containers) == 0 { return entities.ImageStatusUnUsed, nil } diff --git a/entities/constants.go b/entities/constants.go new file mode 100644 index 0000000..3782178 --- /dev/null +++ b/entities/constants.go @@ -0,0 +1,13 @@ +package entities + +import "time" + +const ( + LEN_IMG_TRIM int = 12 + LEN_CONT_TRIM int = 12 + LOGS_QUEUE_LEN int = 10 + STATES_PULL_INTERVAL time.Duration = time.Second + // Sleeping time, not too much, not so little (under 500 millisecond would be annoying) + LOGS_PULL_INTERVAL time.Duration = 500 * time.Millisecond + CLIENT_TIMEOUT time.Duration = 30 * time.Second +) diff --git a/entities/container.go b/entities/container.go index a6f3fb3..51d0ee6 100644 --- a/entities/container.go +++ b/entities/container.go @@ -15,11 +15,12 @@ const ( ) type Container struct { - ID string - Name string - Image string - Status string - State ContainerState + ID string + Name string + Image string + Status string + State ContainerState + RemoveForm *ContainerRemoveForm } func (c Container) String() string { @@ -29,3 +30,15 @@ func (c Container) String() string { func (c Container) IsOn() bool { return c.State == ContainerStateRunning } + +func (c *Container) On() { + c.State = ContainerStateRunning +} + +func (c *Container) Off() { + c.State = ContainerStateExited +} + +func (c *Container) ShortID() string { + return c.ID[:LEN_CONT_TRIM] +} diff --git a/entities/image.go b/entities/image.go index ac94dcc..df0d7e3 100644 --- a/entities/image.go +++ b/entities/image.go @@ -1,5 +1,11 @@ package entities +import ( + "fmt" + + "github.com/arshamalh/dockeroller/tools" +) + type ImageStatus string const ( @@ -15,5 +21,15 @@ type Image struct { Status ImageStatus CreatedAt string // A list of Containers using this image - UsedBy []*Container + UsedBy []*Container + RemoveForm *ImageRemoveForm +} + +func (img Image) String() string { + size := tools.SizeToHumanReadable(img.Size) + return fmt.Sprintf("%s - %s - %s - created at: %s", img.ID, size, img.Status, img.CreatedAt) +} + +func (img *Image) ShortID() string { + return img.ID[:LEN_IMG_TRIM] } diff --git a/entities/scene.go b/entities/scene.go index 82b7a5e..13a390f 100644 --- a/entities/scene.go +++ b/entities/scene.go @@ -6,17 +6,3 @@ const ( SceneRenameContainer Scene = iota + 1 SceneRenameImage ) - -type Question int - -const ( - QNewContainerName Question = iota + 1 -) - -const ( - QNewImageName Question = iota + 1 -) - -func (q Question) NextQuestion() Question { - return q + 1 -} diff --git a/go.mod b/go.mod index 099f7de..62f13a5 100644 --- a/go.mod +++ b/go.mod @@ -1,44 +1,50 @@ module github.com/arshamalh/dockeroller -go 1.21 +go 1.22 require ( - github.com/docker/docker v20.10.17+incompatible + github.com/docker/docker v26.0.2+incompatible + github.com/jaswdr/faker v1.19.1 github.com/jaswdr/faker/v2 v2.1.0 github.com/joho/godotenv v1.4.0 + github.com/moby/moby v26.0.2+incompatible + github.com/spf13/cobra v1.7.0 + github.com/stretchr/testify v1.9.0 go.uber.org/mock v0.3.0 + go.uber.org/zap v1.26.0 gopkg.in/telebot.v3 v3.0.0 ) require ( + github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/containerd/log v0.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 // indirect + go.opentelemetry.io/otel v1.25.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 // indirect + go.opentelemetry.io/otel/metric v1.25.0 // indirect + go.opentelemetry.io/otel/sdk v1.25.0 // indirect + go.opentelemetry.io/otel/trace v1.25.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect golang.org/x/time v0.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.1 // indirect ) - -require ( - github.com/Microsoft/go-winio v0.5.2 // indirect - github.com/docker/distribution v2.8.1+incompatible // indirect - github.com/docker/go-connections v0.4.0 // indirect - github.com/docker/go-units v0.4.0 // indirect - github.com/go-faker/faker/v4 v4.4.1 - github.com/gogo/protobuf v1.3.2 // indirect - github.com/moby/moby v20.10.18+incompatible - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.2 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/sirupsen/logrus v1.9.0 // indirect - github.com/spf13/cobra v1.7.0 - github.com/stretchr/testify v1.8.4 - go.uber.org/zap v1.26.0 - golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect - golang.org/x/sys v0.5.0 // indirect -) diff --git a/go.sum b/go.sum index 387ea61..eb0ade9 100644 --- a/go.sum +++ b/go.sum @@ -2,22 +2,31 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= -github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= -github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v26.0.2+incompatible h1:yGVmKUFGgcxA6PXWAokO0sQL22BrQ67cgVjko8tGdXE= +github.com/docker/docker v26.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/go-faker/faker/v4 v4.4.1 h1:LY1jDgjVkBZWIhATCt+gkl0x9i/7wC61gZx73GTFb+Q= -github.com/go-faker/faker/v4 v4.4.1/go.mod h1:HRLrjis+tYsbFtIHufEPTAIzcZiRu0rS9EYl2Ccwme4= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= @@ -25,10 +34,14 @@ github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jaswdr/faker v1.19.1 h1:xBoz8/O6r0QAR8eEvKJZMdofxiRH+F0M/7MU9eNKhsM= +github.com/jaswdr/faker v1.19.1/go.mod h1:x7ZlyB1AZqwqKZgyQlnqEG8FDptmHlncA5u2zY/yi6w= github.com/jaswdr/faker/v2 v2.1.0 h1:WH3gmTasNM2UctjTFAGpItyLkzei+Z48hG0VIKL1sAw= github.com/jaswdr/faker/v2 v2.1.0/go.mod h1:ROK8xwQV0hYOLDUtxCQgHGcl10jbVzIvqHxcIDdwY2Q= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= @@ -41,8 +54,10 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/moby/moby v20.10.18+incompatible h1:aAQ5lDb+SDrhVDnoMbR3kSzswd+41X34pex8VRJXvHg= -github.com/moby/moby v20.10.18+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/moby v26.0.2+incompatible h1:t41TD3nRvK8E6bZFJdKrmNlH8Xe3epTmdNXf/mnfLKk= +github.com/moby/moby v26.0.2+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= @@ -57,8 +72,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= @@ -68,10 +83,26 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 h1:cEPbyTSEHlQR89XVlyo78gqluF8Y3oMeBkXGWzQsfXY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0/go.mod h1:DKdbWcT4GH1D0Y3Sqt/PFXt2naRKDWtU+eE6oLdFNA8= +go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= +go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 h1:dT33yIHtmsqpixFsSQPwNeY5drM9wTcoL8h0FWF4oGM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 h1:Mbi5PKN7u322woPa85d7ebZ+SOvEoPvoiBu+ryHWgfA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0/go.mod h1:e7ciERRhZaOZXVjx5MiL8TK5+Xv7G5Gv5PA2ZDEJdL8= +go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA= +go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= +go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo= +go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw= +go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM= +go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= +go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= +go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= @@ -89,8 +120,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -100,15 +131,12 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -124,6 +152,14 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8= +google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +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/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/telebot.v3 v3.0.0 h1:UgHIiE/RdjoDi6nf4xACM7PU3TqiPVV9vvTydCEnrTo= diff --git a/log/log.go b/log/log.go index 8845702..9ce4861 100644 --- a/log/log.go +++ b/log/log.go @@ -31,5 +31,5 @@ func InitGlobalLogger(filename string) { zapcore.NewCore(fileEncoder, writer, defaultLogLevel), zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), defaultLogLevel), ) - Gl = zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)) + Gl = zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.WarnLevel)) } diff --git a/session/session.go b/session/session.go index fc98151..5a34a7e 100644 --- a/session/session.go +++ b/session/session.go @@ -1,7 +1,5 @@ package session -import "github.com/arshamalh/dockeroller/entities" - type Session interface { Get(userID int64) *UserData } @@ -25,10 +23,7 @@ func (e *session) Get(userID int64) *UserData { func (e *session) init(userID int64) { e.userData[userID] = &UserData{ - UserID: userID, - Scene: 0, - CurrentQuestion: 0, - Containers: make([]*entities.Container, 0), - Images: make([]*entities.Image, 0), + userID: userID, + scene: 0, } } diff --git a/session/session_test.go b/session/session_test.go index 0d46dab..9b7565e 100644 --- a/session/session_test.go +++ b/session/session_test.go @@ -32,18 +32,4 @@ func TestNewSession(t *testing.T) { assert.Equal(given, got) }) - - t.Run("question", func(t *testing.T) { - s := session.New() - - var userID int64 = 2 - userData := s.Get(userID) - assert.NotNil(userData) - - userData.SetContainerRemoveForm(true, true) - got := userData.GetContainerRemoveForm() - - assert.Equal(true, got.Force) - assert.Equal(true, got.RemoveVolumes) - }) } diff --git a/session/userdata.go b/session/userdata.go index 9a40adb..86bca2f 100644 --- a/session/userdata.go +++ b/session/userdata.go @@ -3,123 +3,55 @@ package session import "github.com/arshamalh/dockeroller/entities" type UserData struct { - UserID int64 - Scene entities.Scene - CurrentQuestion entities.Question - Containers []*entities.Container - Images []*entities.Image - CurrentContainer *entities.Container - CurrentImage *entities.Image - Forms *entities.Forms - QuitChan chan struct{} + userID int64 + scene entities.Scene + currentContainer *entities.Container + currentContainerIndex int + currentImage *entities.Image + currentImageIndex int + quitChan chan struct{} } -func (d *UserData) SetScene(scene entities.Scene) { - d.Scene = scene - switch scene { - case entities.SceneRenameContainer: - d.CurrentQuestion = entities.QNewContainerName - case entities.SceneRenameImage: - d.CurrentQuestion = entities.QNewImageName - } -} - -func (d *UserData) GetScene() entities.Scene { - return d.Scene -} - -func (d *UserData) SetCurrentQuestion(question entities.Question) { - d.CurrentQuestion = question -} - -func (d *UserData) GetCurrentQuestion() entities.Question { - return d.CurrentQuestion +func (d *UserData) ID() int64 { + return d.userID } -func (d *UserData) SetCurrentContainer(container *entities.Container) { - d.CurrentContainer = container -} - -func (d *UserData) GetCurrentContainer() *entities.Container { - return d.CurrentContainer +func (d *UserData) SetScene(scene entities.Scene) { + d.scene = scene } -func (d *UserData) SetCurrentImage(image *entities.Image) { - d.CurrentImage = image +func (d *UserData) GetScene() entities.Scene { + return d.scene } -func (d *UserData) GetCurrentImage() *entities.Image { - return d.CurrentImage +func (d *UserData) SetCurrentContainer(container *entities.Container, index int) { + if container.RemoveForm == nil { + container.RemoveForm = &entities.ContainerRemoveForm{} + } + d.currentContainer = container + d.currentContainerIndex = index } -func (d *UserData) GetContainers() []*entities.Container { - return d.Containers +func (d *UserData) GetCurrentContainer() (*entities.Container, int) { + return d.currentContainer, d.currentContainerIndex } -func (d *UserData) GetContainer(index int) *entities.Container { - return d.Containers[index] +func (d *UserData) SetCurrentImage(image *entities.Image, index int) { + if image.RemoveForm == nil { + image.RemoveForm = &entities.ImageRemoveForm{} + } + d.currentImage = image + d.currentImageIndex = index } -func (d *UserData) SetContainers(containers []*entities.Container) { - d.Containers = containers +func (d *UserData) GetCurrentImage() (*entities.Image, int) { + return d.currentImage, d.currentImageIndex } func (d *UserData) SetQuitChan(quitChan chan struct{}) { - d.QuitChan = quitChan + d.quitChan = quitChan } func (d *UserData) GetQuitChan() chan<- struct{} { - return d.QuitChan -} - -func (d *UserData) GetImages() []*entities.Image { - return d.Images -} - -func (d *UserData) SetImages(images []*entities.Image) { - d.Images = images -} - -func (d *UserData) GetForms() *entities.Forms { - return d.Forms -} - -func (d *UserData) SetForms(forms *entities.Forms) { - d.Forms = forms -} - -func (d *UserData) SetContainerRemoveForm(force, removeVolumes bool) *entities.ContainerRemoveForm { - if d.Forms == nil { - d.Forms = &entities.Forms{ - ContainerRemove: &entities.ContainerRemoveForm{}, - } - } - d.Forms.ContainerRemove.Force = force - d.Forms.ContainerRemove.RemoveVolumes = removeVolumes - return d.Forms.ContainerRemove -} - -func (d *UserData) GetContainerRemoveForm() *entities.ContainerRemoveForm { - if d.Forms != nil && d.Forms.ContainerRemove != nil { - return d.Forms.ContainerRemove - } - return nil -} - -func (d *UserData) SetImageRemoveForm(force, pruneChildren bool) *entities.ImageRemoveForm { - if d.Forms == nil { - d.Forms = &entities.Forms{ - ImageRemove: &entities.ImageRemoveForm{}, - } - } - d.Forms.ImageRemove.Force = force - d.Forms.ImageRemove.PruneChildren = pruneChildren - return d.Forms.ImageRemove -} - -func (d *UserData) GetImageRemoveForm() *entities.ImageRemoveForm { - if d.Forms != nil && d.Forms.ImageRemove != nil { - return d.Forms.ImageRemove - } - return nil + return d.quitChan } diff --git a/session/userdata_test.go b/session/userdata_test.go index 8b01817..8cc88b9 100644 --- a/session/userdata_test.go +++ b/session/userdata_test.go @@ -1,61 +1,38 @@ package session_test import ( + "fmt" + "testing" + "time" + "github.com/arshamalh/dockeroller/entities" "github.com/arshamalh/dockeroller/session" - "github.com/go-faker/faker/v4" + "github.com/jaswdr/faker" "github.com/stretchr/testify/assert" - "testing" ) func TestUserData(t *testing.T) { assert := assert.New(t) - t.Run("set and get containers", func(t *testing.T) { - s := session.New() - userData := s.Get(2) - fakeContainers := newFakeContainers() - - userData.SetContainers(fakeContainers) - userContainers := userData.GetContainers() - - assert.NotNil(userContainers) - for _, userContainer := range userContainers { - assert.NotNil(userContainer) - } - }) t.Run("set and get current container", func(t *testing.T) { s := session.New() userData := s.Get(2) - fakeContainers := newFakeContainers() + fakeContainers := fakeContainers(1) - userData.SetCurrentContainer(fakeContainers[1]) - userCurrentContainer := userData.GetCurrentContainer() + userData.SetCurrentContainer(fakeContainers[0], 0) + userCurrentContainer, index := userData.GetCurrentContainer() assert.NotNil(userCurrentContainer) - }) - - t.Run("set and get images", func(t *testing.T) { - s := session.New() - userData := s.Get(2) - fakeImages := newFakeImages() - - userData.SetImages(fakeImages) - userImages := userData.GetImages() - - assert.NotNil(userImages) - for _, userImage := range userImages { - assert.NotNil(userImage) - } + assert.Equal(0, index) }) t.Run("set and get current image", func(t *testing.T) { s := session.New() userData := s.Get(2) - fakeImages := newFakeImages() + fakeImages := fakeImages(3) - userData.SetCurrentImage(fakeImages[1]) - userCurrentImage := userData.GetCurrentImage() + userData.SetCurrentImage(fakeImages[1], 1) + userCurrentImage, _ := userData.GetCurrentImage() assert.NotNil(userCurrentImage) }) @@ -66,44 +43,57 @@ func TestUserData(t *testing.T) { userData.SetScene(entities.SceneRenameImage) scene := userData.GetScene() - assert.NotNil(scene) - assert.Equal(userData.CurrentQuestion, entities.QNewImageName) userData.SetScene(entities.SceneRenameContainer) scene = userData.GetScene() - assert.NotNil(scene) - assert.Equal(userData.CurrentQuestion, entities.QNewContainerName) }) } -func newFakeContainers() []*entities.Container { - var containers []*entities.Container - for i := 0; i < 3; i++ { - containers = append(containers, &entities.Container{ - ID: faker.UUIDDigit(), - Name: faker.Word(), - State: entities.ContainerStateRunning, - Image: faker.Word(), - Status: faker.Word(), - }) +func fakeContainers(howMany int) []*entities.Container { + faker := faker.New() + allContainerStates := []entities.ContainerState{ + entities.ContainerStateCreated, + entities.ContainerStateRunning, + entities.ContainerStateDead, + entities.ContainerStateExited, + entities.ContainerStatePaused, } - return containers + containersList := make([]*entities.Container, howMany) + + for i := range containersList { + randInt := faker.IntBetween(0, len(allContainerStates)-1) + containersList[i] = &entities.Container{ + ID: faker.UUID().V4(), + Status: faker.Lorem().Sentence(10), + Name: faker.Person().FirstName(), + Image: fmt.Sprintf( + "%s:%f-%s%f", + faker.Address().City(), faker.Float32(2, 10, 99), + faker.Blood().Name(), faker.Float32(2, 10, 99), + ), + State: allContainerStates[randInt], + } + } + return containersList } -func newFakeImages() []*entities.Image { - var images []*entities.Image - for i := 0; i < 3; i++ { - images = append(images, &entities.Image{ - ID: faker.UUIDDigit(), - Size: 12, - Tags: []string{faker.Word(), faker.WORD}, - Status: entities.ImageStatusInUse, - CreatedAt: faker.TIMESTAMP, - }) - } +func fakeImages(howMany int) []*entities.Image { + faker := faker.New() - return images + imagesList := make([]*entities.Image, howMany) + + for i := range imagesList { + imagesList[i] = &entities.Image{ + ID: faker.UUID().V4(), + // TODO: image status is enum + Status: entities.ImageStatus(faker.Lorem().Sentence(10)), + Size: faker.Int64(), + CreatedAt: faker.Time().RFC1123(time.Now()), + UsedBy: fakeContainers(3), + } + } + return imagesList } diff --git a/telegram/handlers/container_logs_stats.go b/telegram/handlers/container_logs_stats.go index 0370fce..f071709 100644 --- a/telegram/handlers/container_logs_stats.go +++ b/telegram/handlers/container_logs_stats.go @@ -2,14 +2,16 @@ package handlers import ( "bufio" + "context" "encoding/json" - "strconv" + "errors" "time" "github.com/arshamalh/dockeroller/entities" "github.com/arshamalh/dockeroller/log" "github.com/arshamalh/dockeroller/telegram/keyboards" "github.com/arshamalh/dockeroller/telegram/msgs" + "github.com/arshamalh/dockeroller/tools" "go.uber.org/zap" "gopkg.in/telebot.v3" ) @@ -18,16 +20,14 @@ func (h *handler) ContainerLogs(ctx telebot.Context) error { // TODO: Starting from the beginning might cause confusion in long stream of errors, we should have a navigate till to the end button. userID := ctx.Chat().ID session := h.session.Get(userID) - index, err := strconv.Atoi(ctx.Data()) - if err != nil { - log.Gl.Error(err.Error()) - } - current := session.GetContainer(index) + containerID, index := tools.ExtractIndexAndID(ctx.Data()) + quit := make(chan struct{}) session.SetQuitChan(quit) - stream, err := h.docker.ContainerLogs(current.ID) + stream, err := h.docker.ContainerLogs(context.TODO(), containerID) if err != nil { log.Gl.Error(err.Error()) + return ctx.Respond(msgs.NoLogsAvailable) } streamer := bufio.NewScanner(stream) @@ -35,67 +35,67 @@ func (h *handler) ContainerLogs(ctx telebot.Context) error { for streamer.Scan() { select { case <-quit: - return nil + return ctx.Respond(msgs.FinishingTheLogsStream) default: - newMsg := streamer.Text() - queue.Push(newMsg) - if queue.Length > 10 { // TODO: 10 should not be hard-coded - queue.Pop() - } + } + newMsg := streamer.Text() + queue.Push(newMsg) + if queue.Length > entities.LOGS_QUEUE_LEN { + queue.Pop() + } - // Omitted error by purpose (the error is just about not modified message because of repetitive content) - ctx.Edit( - queue.String(), - keyboards.ContainerBack(index), - ) - time.Sleep(time.Millisecond * 500) - // TODO: sleeping time, not hardcoded, not too much, not so little (under 500 millisecond would be annoying) + // Omitted error by purpose (the error is just about not modified message because of repetitive content) + err := ctx.Edit( + msgs.FmtMono(queue.String()), + keyboards.ContainerBack(index), + ) + if err != nil && !errors.Is(err, telebot.ErrMessageNotModified) { + log.Gl.Error(err.Error()) } + time.Sleep(entities.LOGS_PULL_INTERVAL) } return ctx.Respond() } func (h *handler) ContainerStats(ctx telebot.Context) error { userID := ctx.Chat().ID - index, err := strconv.Atoi(ctx.Data()) session := h.session.Get(userID) - if err != nil { - log.Gl.Error(err.Error()) - } - current := session.GetContainer(index) + containerID, index := tools.ExtractIndexAndID(ctx.Data()) + quit := make(chan struct{}) session.SetQuitChan(quit) - stream, err := h.docker.ContainerStats(current.ID) + stream, err := h.docker.ContainerStats(context.TODO(), containerID) if err != nil { log.Gl.Error(err.Error()) + return ctx.Respond(msgs.NoStatsAvailable) } + streamer := bufio.NewScanner(stream) latestMsg := "" + for streamer.Scan() { select { case <-quit: log.Gl.Debug("end of streaming stats for user", zap.Int64("used_id", userID)) - return nil + return ctx.Respond(msgs.FinishingTheStatsStream) default: - stats := entities.Stats{} - err := json.Unmarshal(streamer.Bytes(), &stats) - if err != nil { - log.Gl.Error(err.Error()) - } + } + stats := entities.Stats{} + err := json.Unmarshal(streamer.Bytes(), &stats) + if err != nil { + log.Gl.Error(err.Error()) + } - if newMsg := msgs.FmtStats(stats); newMsg != latestMsg { - err := ctx.Edit( - newMsg, - keyboards.ContainerBack(index), - telebot.ModeMarkdownV2, - ) - if err != nil { - log.Gl.Error(err.Error()) - } - latestMsg = newMsg + if newMsg := msgs.FmtStats(stats); newMsg != latestMsg { + err := ctx.Edit( + newMsg, keyboards.ContainerBack(index), + ) + if err != nil && !errors.Is(err, telebot.ErrMessageNotModified) { + log.Gl.Error(err.Error()) } - time.Sleep(time.Second) + latestMsg = newMsg } + time.Sleep(entities.STATES_PULL_INTERVAL) } return ctx.Respond() } diff --git a/telegram/handlers/container_remove.go b/telegram/handlers/container_remove.go index 87ec4c0..090fafb 100644 --- a/telegram/handlers/container_remove.go +++ b/telegram/handlers/container_remove.go @@ -2,12 +2,13 @@ package handlers import ( "context" - "strconv" "github.com/arshamalh/dockeroller/log" "github.com/arshamalh/dockeroller/telegram/keyboards" "github.com/arshamalh/dockeroller/telegram/msgs" + "github.com/arshamalh/dockeroller/tools" "github.com/docker/docker/api/types/filters" + "go.uber.org/zap" "gopkg.in/telebot.v3" ) @@ -16,95 +17,85 @@ import ( func (h *handler) ContainerRemoveForm(ctx telebot.Context) error { ctx.Respond(msgs.FillTheFormAndPressDone) userID := ctx.Chat().ID - index, err := strconv.Atoi(ctx.Data()) session := h.session.Get(userID) - if err != nil { - log.Gl.Error(err.Error()) - } - current := session.GetContainer(index) - cRemoveForm := session.GetContainerRemoveForm() - if cRemoveForm == nil { - cRemoveForm = session.SetContainerRemoveForm(false, false) - } + containerID := ctx.Data() + + current, index := session.GetCurrentContainer() + crf := current.RemoveForm return ctx.Edit( msgs.FmtContainer(current), - keyboards.ContainerRemove(index, cRemoveForm.Force, cRemoveForm.RemoveVolumes), - telebot.ModeMarkdownV2, + keyboards.ContainerRemove(containerID, index, crf.Force, crf.RemoveVolumes), ) } // Removes the container with specified options func (h *handler) ContainerRemoveDone(ctx telebot.Context) error { userID := ctx.Chat().ID - index, err := strconv.Atoi(ctx.Data()) + containerID, _ := tools.ExtractIndexAndID(ctx.Data()) session := h.session.Get(userID) - if err != nil { - log.Gl.Error(err.Error()) + + current, _ := session.GetCurrentContainer() + if current.ShortID() != containerID { + log.Gl.Info( + "Removing another container when current is different", + zap.String("current in session", current.String()), + zap.String("from callback", containerID), + ) + return ctx.Respond(msgs.UnableToRemoveContainer) } - current := session.GetContainer(index) - cRemoveForm := session.GetContainerRemoveForm() - if err := h.docker.ContainerRemove(current.ID, cRemoveForm); err != nil { + if err := h.docker.ContainerRemove(context.TODO(), containerID, current.RemoveForm); err != nil { log.Gl.Error(err.Error()) return ctx.Respond(msgs.UnableToRemoveContainer) } ctx.Respond(msgs.ContainerRemovedSuccessfully) - containers := h.docker.ContainersList(context.TODO(), filters.Args{}) - session.SetContainers(containers) + containers, err := h.docker.ContainersList(context.TODO(), filters.Args{}) + if err != nil { + log.Gl.Error(err.Error()) + return ctx.Send("can't fetch containers") // TODO: Inform user we can't fetch containers + } + if len(containers) == 0 { - // TODO: if container removed and there is no container, get back to the start menu, no extra action needed + // TODO: if container removed and there is no container, + // get back to the start menu, no extra action needed. return ctx.Send("there is no container") } - current = containers[0] + current = containers[0] + session.SetCurrentContainer(current, 0) return ctx.Edit( msgs.FmtContainer(current), - keyboards.ContainersList(0, current.IsOn()), - telebot.ModeMarkdownV2, + keyboards.ContainersList(current.ID, 0, current.IsOn()), ) } func (h *handler) ContainerRemoveForce(ctx telebot.Context) error { userID := ctx.Chat().ID - index, err := strconv.Atoi(ctx.Data()) session := h.session.Get(userID) - if err != nil { - log.Gl.Error(err.Error()) - return ctx.Respond(msgs.InvalidButton) - } - - current := session.GetContainer(index) - cRemoveForm := session.GetContainerRemoveForm() - cRemoveForm.Force = !cRemoveForm.Force - session.SetContainerRemoveForm(cRemoveForm.Force, cRemoveForm.RemoveVolumes) + current, index := session.GetCurrentContainer() + crf := current.RemoveForm + crf.Force = !crf.Force + session.SetCurrentContainer(current, index) return ctx.Edit( msgs.FmtContainer(current), - keyboards.ContainerRemove(index, cRemoveForm.Force, cRemoveForm.RemoveVolumes), - telebot.ModeMarkdownV2, + keyboards.ContainerRemove(current.ID, index, crf.Force, crf.RemoveVolumes), ) } func (h *handler) ContainerRemoveVolumes(ctx telebot.Context) error { userID := ctx.Chat().ID session := h.session.Get(userID) - index, err := strconv.Atoi(ctx.Data()) - if err != nil { - log.Gl.Error(err.Error()) - return ctx.Respond(msgs.InvalidButton) - } - - current := session.GetContainer(index) - cRemoveForm := session.GetContainerRemoveForm() - cRemoveForm.RemoveVolumes = !cRemoveForm.RemoveVolumes - session.SetContainerRemoveForm(cRemoveForm.Force, cRemoveForm.RemoveVolumes) + current, index := session.GetCurrentContainer() + crf := current.RemoveForm + crf.RemoveVolumes = !crf.RemoveVolumes + session.SetCurrentContainer(current, index) return ctx.Edit( msgs.FmtContainer(current), - keyboards.ContainerRemove(index, cRemoveForm.Force, cRemoveForm.RemoveVolumes), - telebot.ModeMarkdownV2, + keyboards.ContainerRemove(current.ID, index, crf.Force, crf.RemoveVolumes), ) } diff --git a/telegram/handlers/container_rename.go b/telegram/handlers/container_rename.go index 2889944..9ad029a 100644 --- a/telegram/handlers/container_rename.go +++ b/telegram/handlers/container_rename.go @@ -2,68 +2,54 @@ package handlers import ( "context" - "strconv" "github.com/arshamalh/dockeroller/entities" "github.com/arshamalh/dockeroller/log" "github.com/arshamalh/dockeroller/telegram/keyboards" "github.com/arshamalh/dockeroller/telegram/msgs" - "github.com/docker/docker/api/types/filters" "gopkg.in/telebot.v3" ) +// ContainerRename Handler is called where rename button is clicked +// It asks new name from user and waits for input func (h *handler) ContainerRename(ctx telebot.Context) error { - if err := ctx.Respond(); err != nil { - log.Gl.Error(err.Error()) - } userID := ctx.Chat().ID - currentContainerIndex := ctx.Data() session := h.session.Get(userID) - index, err := strconv.Atoi(currentContainerIndex) - if err != nil { - log.Gl.Error(err.Error()) - return ctx.Send("wrong button clicked!") - } - containers := session.GetContainers() - current := containers[index] + _, index := session.GetCurrentContainer() session.SetScene(entities.SceneRenameContainer) - session.SetCurrentContainer(current) + h.EmptyResponder(ctx) return ctx.Edit( msgs.ContainerNewNameInput, keyboards.ContainerBack(index), - telebot.ModeMarkdownV2, ) } func (h *handler) ContainerRenameTextHandler(ctx telebot.Context) error { userID := ctx.Chat().ID session := h.session.Get(userID) - container := session.GetCurrentContainer() + container, index := session.GetCurrentContainer() if container == nil { return ctx.Edit( "you're lost!, please /start again", - keyboards.ContainerBack(0), - telebot.ModeMarkdownV2, + keyboards.ContainerBack(index), ) } newName := ctx.Text() - if err := h.docker.ContainerRename(container.ID, newName); err != nil { + if err := h.docker.ContainerRename(context.TODO(), container.ID, newName); err != nil { log.Gl.Error(err.Error()) return ctx.Edit( "we cannot rename this container", - keyboards.ContainerBack(0), - telebot.ModeMarkdownV2, + keyboards.ContainerBack(index), ) } - containers := h.docker.ContainersList(context.TODO(), filters.Args{}) - session.SetContainers(containers) + container.Name = newName + session.SetCurrentContainer(container, index) return ctx.Send( msgs.FmtContainerRenamed(container.Name, newName), - keyboards.ContainerBack(0), - telebot.ModeMarkdownV2, + keyboards.ContainerBack(index), ) } diff --git a/telegram/handlers/containers.go b/telegram/handlers/containers.go index dc1ef4f..42242e2 100644 --- a/telegram/handlers/containers.go +++ b/telegram/handlers/containers.go @@ -2,8 +2,8 @@ package handlers import ( "context" - "strconv" + "github.com/arshamalh/dockeroller/entities" "github.com/arshamalh/dockeroller/log" "github.com/arshamalh/dockeroller/telegram/keyboards" "github.com/arshamalh/dockeroller/telegram/msgs" @@ -13,44 +13,44 @@ import ( ) func (h *handler) ContainersList(ctx telebot.Context) error { - userID := ctx.Chat().ID - session := h.session.Get(userID) - containers := h.docker.ContainersList(context.TODO(), filters.Args{}) - session.SetContainers(containers) + containers, err := h.docker.ContainersList(context.TODO(), filters.Args{}) + if err != nil { + log.Gl.Error(err.Error()) + return ctx.Respond(msgs.UnableToFetchContainers) + } if len(containers) == 0 { return ctx.Respond(msgs.NoContainer) } current := containers[0] + h.session.Get(ctx.Chat().ID).SetCurrentContainer(current, 0) + return ctx.Send( msgs.FmtContainer(current), - keyboards.ContainersList(0, current.IsOn()), - telebot.ModeMarkdownV2, + keyboards.ContainersList(current.ID, 0, current.IsOn()), ) } func (h *handler) ContainersNavBtn(ctx telebot.Context) error { - userID := ctx.Chat().ID - index, err := strconv.Atoi(ctx.Data()) + containers, err := h.docker.ContainersList(context.TODO(), filters.Args{}) if err != nil { - log.Gl.Error(err.Error()) + return ctx.Respond(msgs.UnableToFetchContainers) } - session := h.session.Get(userID) - containers := h.docker.ContainersList(context.TODO(), filters.Args{}) - session.SetContainers(containers) if len(containers) == 0 { return ctx.Respond(msgs.NoContainer) } + + userID := ctx.Chat().ID + session := h.session.Get(userID) + index := tools.Str2Int(ctx.Data()) index = tools.Indexer(index, len(containers)) current := containers[index] + session.SetCurrentContainer(current, index) - if err := ctx.Respond(); err != nil { - log.Gl.Error(err.Error()) - } + h.EmptyResponder(ctx) return ctx.Edit( msgs.FmtContainer(current), - keyboards.ContainersList(index, current.IsOn()), - telebot.ModeMarkdownV2, + keyboards.ContainersList(current.ID, index, current.IsOn()), ) } @@ -60,64 +60,76 @@ func (h *handler) ContainersBackBtn(ctx telebot.Context) error { if quitChan := session.GetQuitChan(); quitChan != nil { quitChan <- struct{}{} } - index, err := strconv.Atoi(ctx.Data()) - if err != nil { - log.Gl.Error(err.Error()) + + current, index := session.GetCurrentContainer() + if current == nil { + containers, err := h.docker.ContainersList(context.TODO(), filters.Args{}) + if err != nil { + log.Gl.Error(err.Error()) + ctx.Respond(msgs.UnableToFetchContainers) + return h.StartHandler(ctx) + } + if len(containers) == 0 { + ctx.Respond(msgs.NoContainer) + return h.StartHandler(ctx) + } + current = containers[0] + session.SetCurrentContainer(current, index) } - current := session.GetContainer(index) + h.EmptyResponder(ctx) return ctx.Edit( msgs.FmtContainer(current), - keyboards.ContainersList(index, current.IsOn()), - telebot.ModeMarkdownV2, + keyboards.ContainersList(current.ID, index, current.IsOn()), ) } func (h *handler) ContainerStart(ctx telebot.Context) error { - userID := ctx.Chat().ID - index, err := strconv.Atoi(ctx.Data()) - session := h.session.Get(userID) - if err != nil { - log.Gl.Error(err.Error()) - } - current := session.GetContainer(index) - if err := h.docker.ContainerStart(current.ID); err != nil { + containerID := ctx.Data() + if err := h.docker.ContainerStart(context.TODO(), containerID); err != nil { log.Gl.Error(err.Error()) return ctx.Respond(msgs.CannotStartTheContainer) } - current, err = h.docker.GetContainer(current.ID) - if err != nil { - return ctx.Respond(msgs.StartedButUnavailableCurrentState) + userID := ctx.Chat().ID + session := h.session.Get(userID) + current, index := session.GetCurrentContainer() + if current.ID[:entities.LEN_CONT_TRIM] != containerID { + log.Gl.Error("Current container isn't equal to callback id") } + current.On() + session.SetCurrentContainer(current, index) + + callbackMsg := msgs.FormattedCBResponse("container %s successfully turned on", current.Name) + ctx.Respond(callbackMsg) + return ctx.Edit( msgs.FmtContainer(current), - keyboards.ContainersList(index, true), - telebot.ModeMarkdownV2, + keyboards.ContainersList(current.ID, index, current.IsOn()), ) } func (h *handler) ContainerStop(ctx telebot.Context) error { - userID := ctx.Chat().ID - index, err := strconv.Atoi(ctx.Data()) - session := h.session.Get(userID) - if err != nil { - log.Gl.Error(err.Error()) - } - current := session.GetContainer(index) - if err := h.docker.ContainerStop(current.ID); err != nil { + containerID := ctx.Data() + if err := h.docker.ContainerStop(context.TODO(), containerID); err != nil { log.Gl.Error(err.Error()) return ctx.Respond(msgs.CannotStopTheContainer) } - current, err = h.docker.GetContainer(current.ID) - if err != nil { - return ctx.Respond(msgs.StoppedButUnavailableCurrentState) + userID := ctx.Chat().ID + session := h.session.Get(userID) + current, index := session.GetCurrentContainer() + if current.ID[:entities.LEN_CONT_TRIM] != containerID { + log.Gl.Error("Current container isn't equal to callback id") } + current.Off() + session.SetCurrentContainer(current, index) + + callbackMsg := msgs.FormattedCBResponse("container %s successfully turned off", current.Name) + ctx.Respond(callbackMsg) return ctx.Edit( msgs.FmtContainer(current), - keyboards.ContainersList(index, false), - telebot.ModeMarkdownV2, + keyboards.ContainersList(current.ID, index, current.IsOn()), ) } diff --git a/telegram/handlers/containers_test.go b/telegram/handlers/containers_test.go index 0af72d2..8dfc0dd 100644 --- a/telegram/handlers/containers_test.go +++ b/telegram/handlers/containers_test.go @@ -137,7 +137,7 @@ func fakeContainers(howMany int) []*entities.Container { containersList := make([]*entities.Container, howMany) for i := range containersList { - ranInt := faker.IntBetween(0, len(allContainerStates)-1) + randInt := faker.IntBetween(0, len(allContainerStates)-1) containersList[i] = &entities.Container{ ID: faker.UUID().V4(), Status: faker.Lorem().Sentence(10), @@ -147,7 +147,7 @@ func fakeContainers(howMany int) []*entities.Container { faker.Address().City(), faker.Float32(2, 10, 99), faker.Blood().Name(), faker.Float32(2, 10, 99), ), - State: allContainerStates[ranInt], + State: allContainerStates[randInt], } } return containersList diff --git a/telegram/handlers/general.go b/telegram/handlers/general.go index 808f833..d252b80 100644 --- a/telegram/handlers/general.go +++ b/telegram/handlers/general.go @@ -13,7 +13,7 @@ import ( func (h *handler) StartHandler(ctx telebot.Context) error { newWelcomeMsg := strings.Replace(msgs.WelcomeMessage, "{name}", ctx.Message().Sender.FirstName, -1) - return ctx.Send(newWelcomeMsg, keyboards.Welcome()) + return ctx.Send(msgs.FmtMono(newWelcomeMsg), keyboards.Welcome()) } func (h *handler) General(ctx telebot.Context) error { @@ -30,3 +30,12 @@ func (h *handler) General(ctx telebot.Context) error { log.Gl.Error("orphan scene", zap.Int64("user id", userID), zap.Int("current scene", int(scene))) return h.StartHandler(ctx) } + +// We SHOULD respond to button click events, +// but when we don't want to, +// we should at least call the empty Respond so the button flash light (UI of waiting) would go away. +func (h *handler) EmptyResponder(ctx telebot.Context) { + if err := ctx.Respond(); err != nil { + log.Gl.Error(err.Error()) + } +} diff --git a/telegram/handlers/handlers.go b/telegram/handlers/handlers.go index af59d51..9adcd30 100644 --- a/telegram/handlers/handlers.go +++ b/telegram/handlers/handlers.go @@ -11,7 +11,6 @@ type handler struct { docker docker.Docker bot *telebot.Bot session sessionPkg.Session - // log } func NewHandler(bot *telebot.Bot, docker docker.Docker, session sessionPkg.Session) *handler { diff --git a/telegram/handlers/image_remove.go b/telegram/handlers/image_remove.go index 4df0037..0f993a0 100644 --- a/telegram/handlers/image_remove.go +++ b/telegram/handlers/image_remove.go @@ -2,11 +2,12 @@ package handlers import ( "context" - "strconv" "github.com/arshamalh/dockeroller/log" "github.com/arshamalh/dockeroller/telegram/keyboards" "github.com/arshamalh/dockeroller/telegram/msgs" + "github.com/docker/docker/api/types/filters" + "go.uber.org/zap" "gopkg.in/telebot.v3" ) @@ -15,23 +16,13 @@ import ( func (h *handler) ImageRemoveForm(ctx telebot.Context) error { userID := ctx.Chat().ID session := h.session.Get(userID) - index, err := strconv.Atoi(ctx.Data()) - if err != nil { - log.Gl.Error(err.Error()) - return ctx.Respond(msgs.InvalidButton) - } + current, _ := session.GetCurrentImage() ctx.Respond(msgs.FillTheFormAndPressDone) - - current := session.GetImages()[index] - imgRmForm := session.GetImageRemoveForm() - if imgRmForm == nil { - imgRmForm = session.SetImageRemoveForm(false, false) - } + irf := current.RemoveForm return ctx.Edit( msgs.FmtImage(current), - keyboards.ImageRemove(index, imgRmForm.Force, imgRmForm.PruneChildren), - telebot.ModeMarkdownV2, + keyboards.ImageRemove(current.ID, irf.Force, irf.PruneChildren), ) } @@ -39,71 +30,73 @@ func (h *handler) ImageRemoveForm(ctx telebot.Context) error { func (h *handler) ImageRemoveDone(ctx telebot.Context) error { userID := ctx.Chat().ID session := h.session.Get(userID) - index, err := strconv.Atoi(ctx.Data()) - if err != nil { - log.Gl.Error(err.Error()) + imageID := ctx.Data() + current, _ := session.GetCurrentImage() + irf := current.RemoveForm + + if current.ShortID() != imageID { + log.Gl.Info( + "Removing another image when current is different", + zap.String("current in session", current.String()), + zap.String("from callback", imageID), + ) + return ctx.Respond(msgs.UnableToRemoveImage) } - current := session.GetImages()[index] - imgRmForm := session.GetImageRemoveForm() - if err := h.docker.ImageRemove(context.TODO(), current.ID, imgRmForm.Force, imgRmForm.PruneChildren); err != nil { + if err := h.docker.ImageRemove(context.TODO(), imageID, irf.Force, irf.PruneChildren); err != nil { log.Gl.Error(err.Error()) - return ctx.Respond() + return ctx.Respond(msgs.UnableToRemoveImage) } ctx.Respond(msgs.ImageRemovedSuccessfully) - images := h.updateImagesList(userID) + images, err := h.docker.ImagesList(context.TODO(), filters.Args{}) + if err != nil { + log.Gl.Error(err.Error()) + return ctx.Send("can't fetch containers") // TODO: Inform user we can't fetch containers + } + if len(images) == 0 { + // TODO: if image removed and there is no image, + // get back to the start menu, no extra action needed. return ctx.Send("there is no image") } - current = images[0] + current = images[0] + session.SetCurrentImage(current, 0) return ctx.Edit( msgs.FmtImage(current), keyboards.ImagesList(0), - telebot.ModeMarkdownV2, ) } func (h *handler) ImageRemoveForce(ctx telebot.Context) error { userID := ctx.Chat().ID - index, err := strconv.Atoi(ctx.Data()) + imageID := ctx.Data() session := h.session.Get(userID) - if err != nil { - log.Gl.Error(err.Error()) - return ctx.Respond(msgs.InvalidButton) - } - - current := session.GetImages()[index] - imgRmForm := session.GetImageRemoveForm() - imgRmForm.Force = !imgRmForm.Force - session.SetImageRemoveForm(imgRmForm.Force, imgRmForm.PruneChildren) + current, index := session.GetCurrentImage() + irf := current.RemoveForm + irf.Force = !irf.Force + session.SetCurrentImage(current, index) + h.EmptyResponder(ctx) return ctx.Edit( msgs.FmtImage(current), - keyboards.ImageRemove(index, imgRmForm.Force, imgRmForm.PruneChildren), - telebot.ModeMarkdownV2, + keyboards.ImageRemove(imageID, irf.Force, irf.PruneChildren), ) } func (h *handler) ImageRemovePruneChildren(ctx telebot.Context) error { userID := ctx.Chat().ID - index, err := strconv.Atoi(ctx.Data()) session := h.session.Get(userID) - if err != nil { - log.Gl.Error(err.Error()) - return ctx.Respond(msgs.InvalidButton) - } - - current := session.GetImages()[index] - imgRmForm := session.GetImageRemoveForm() - imgRmForm.PruneChildren = !imgRmForm.PruneChildren - session.SetImageRemoveForm(imgRmForm.Force, imgRmForm.PruneChildren) + current, index := session.GetCurrentImage() + irf := current.RemoveForm + irf.PruneChildren = !irf.PruneChildren + session.SetCurrentImage(current, index) + h.EmptyResponder(ctx) return ctx.Edit( msgs.FmtImage(current), - keyboards.ImageRemove(index, imgRmForm.Force, imgRmForm.PruneChildren), - telebot.ModeMarkdownV2, + keyboards.ImageRemove(current.ID, irf.Force, irf.PruneChildren), ) } diff --git a/telegram/handlers/image_tag.go b/telegram/handlers/image_tag.go index 07f3595..9deb1b5 100644 --- a/telegram/handlers/image_tag.go +++ b/telegram/handlers/image_tag.go @@ -2,66 +2,48 @@ package handlers import ( "context" - "strconv" "github.com/arshamalh/dockeroller/entities" "github.com/arshamalh/dockeroller/log" "github.com/arshamalh/dockeroller/telegram/keyboards" "github.com/arshamalh/dockeroller/telegram/msgs" + "github.com/arshamalh/dockeroller/tools" "gopkg.in/telebot.v3" ) func (h *handler) ImageTag(ctx telebot.Context) error { - if err := ctx.Respond(); err != nil { - log.Gl.Error(err.Error()) - } userID := ctx.Chat().ID - session := h.session.Get(userID) - currentImageIndex := ctx.Data() - index, err := strconv.Atoi(currentImageIndex) - if err != nil { - log.Gl.Error(err.Error()) - return ctx.Send("wrong button clicked!") - } - images := session.GetImages() - current := images[index] - session.SetScene(entities.SceneRenameImage) - session.SetCurrentImage(current) + index := tools.Str2Int(ctx.Data()) + h.session.Get(userID).SetScene(entities.SceneRenameImage) + h.EmptyResponder(ctx) return ctx.Edit( msgs.ImageNewNameInput, keyboards.ImageBack(index), - telebot.ModeMarkdownV2, ) } func (h *handler) ImageTagTextHandler(ctx telebot.Context) error { userID := ctx.Chat().ID session := h.session.Get(userID) - image := session.GetCurrentImage() + image, index := session.GetCurrentImage() if image == nil { - return ctx.Edit( + return ctx.Send( "you're lost!, please /start again", keyboards.ImageBack(0), - telebot.ModeMarkdownV2, ) } newTag := ctx.Text() if err := h.docker.ImageTag(context.TODO(), image.ID, newTag); err != nil { log.Gl.Error(err.Error()) - return ctx.Edit( - "we cannot rename this image", - keyboards.ImageBack(0), - telebot.ModeMarkdownV2, + return ctx.Send( + msgs.FmtMono(msgs.InvalidImageTag), ) } - h.updateImagesList(userID) - return ctx.Send( msgs.FmtImageTagged(image.ID, newTag), - keyboards.ImageBack(0), - telebot.ModeMarkdownV2, + keyboards.ImageBack(index), ) } diff --git a/telegram/handlers/images.go b/telegram/handlers/images.go index d68575e..b95705a 100644 --- a/telegram/handlers/images.go +++ b/telegram/handlers/images.go @@ -1,73 +1,83 @@ package handlers import ( - "strconv" + "context" - "github.com/arshamalh/dockeroller/entities" "github.com/arshamalh/dockeroller/log" "github.com/arshamalh/dockeroller/telegram/keyboards" "github.com/arshamalh/dockeroller/telegram/msgs" "github.com/arshamalh/dockeroller/tools" + "github.com/docker/docker/api/types/filters" "gopkg.in/telebot.v3" ) func (h *handler) ImagesList(ctx telebot.Context) error { - ctx.Respond() - userID := ctx.Chat().ID - images := h.updateImagesList(userID) + images, err := h.docker.ImagesList(context.TODO(), filters.Args{}) + if err != nil { + log.Gl.Error(err.Error()) + return ctx.Respond(msgs.UnableToFetchImages) + } + if len(images) == 0 { + return ctx.Respond(msgs.NoImages) + } + current := images[0] + userID := ctx.Chat().ID + session := h.session.Get(userID) + session.SetCurrentImage(current, 0) + + h.EmptyResponder(ctx) return ctx.Send( msgs.FmtImage(current), keyboards.ImagesList(0), - telebot.ModeMarkdownV2, ) } func (h *handler) ImagesNavBtn(ctx telebot.Context) error { userID := ctx.Chat().ID - index, err := strconv.Atoi(ctx.Data()) + session := h.session.Get(userID) + + images, err := h.docker.ImagesList(context.TODO(), filters.Args{}) if err != nil { log.Gl.Error(err.Error()) + return ctx.Respond(msgs.UnableToFetchImages) } - - images := h.updateImagesList(userID) if len(images) == 0 { - return ctx.Respond(&telebot.CallbackResponse{Text: "There is either no images or you should run /images again!"}) + return ctx.Respond(msgs.NoImages) } + + index := tools.Str2Int(ctx.Data()) index = tools.Indexer(index, len(images)) current := images[index] - err = ctx.Edit( + session.SetCurrentImage(current, index) + + h.EmptyResponder(ctx) + + return ctx.Edit( msgs.FmtImage(current), keyboards.ImagesList(index), - telebot.ModeMarkdownV2, ) - if err != nil { - log.Gl.Error(err.Error()) - } - return ctx.Respond() } func (h *handler) ImagesBackBtn(ctx telebot.Context) error { - userID := ctx.Chat().ID - session := h.session.Get(userID) - if quitChan := session.GetQuitChan(); quitChan != nil { - quitChan <- struct{}{} - } - index, err := strconv.Atoi(ctx.Data()) + images, err := h.docker.ImagesList(context.TODO(), filters.Args{}) if err != nil { log.Gl.Error(err.Error()) + return ctx.Respond(msgs.UnableToFetchImages) + } + if len(images) == 0 { + return ctx.Respond(msgs.NoImages) } - current := session.GetImages()[index] + + index := tools.Str2Int(ctx.Data()) + index = tools.Indexer(index, len(images)) + current := images[index] + userID := ctx.Chat().ID + session := h.session.Get(userID) + session.SetCurrentImage(current, index) + return ctx.Edit( msgs.FmtImage(current), keyboards.ImagesList(index), - telebot.ModeMarkdownV2, ) } - -func (h *handler) updateImagesList(userID int64) []*entities.Image { - images := h.docker.ImagesList() - session := h.session.Get(userID) - session.SetImages(images) - return images -} diff --git a/telegram/handlers/mock_docker_test.go b/telegram/handlers/mock_docker_test.go index 44b6ea4..0f5cab7 100644 --- a/telegram/handlers/mock_docker_test.go +++ b/telegram/handlers/mock_docker_test.go @@ -42,97 +42,98 @@ func (m *MockDocker) EXPECT() *MockDockerMockRecorder { } // ContainerLogs mocks base method. -func (m *MockDocker) ContainerLogs(arg0 string) (io.ReadCloser, error) { +func (m *MockDocker) ContainerLogs(arg0 context.Context, arg1 string) (io.ReadCloser, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ContainerLogs", arg0) + ret := m.ctrl.Call(m, "ContainerLogs", arg0, arg1) ret0, _ := ret[0].(io.ReadCloser) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerLogs indicates an expected call of ContainerLogs. -func (mr *MockDockerMockRecorder) ContainerLogs(arg0 any) *gomock.Call { +func (mr *MockDockerMockRecorder) ContainerLogs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerLogs", reflect.TypeOf((*MockDocker)(nil).ContainerLogs), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerLogs", reflect.TypeOf((*MockDocker)(nil).ContainerLogs), arg0, arg1) } // ContainerRemove mocks base method. -func (m *MockDocker) ContainerRemove(arg0 string, arg1 *entities.ContainerRemoveForm) error { +func (m *MockDocker) ContainerRemove(arg0 context.Context, arg1 string, arg2 *entities.ContainerRemoveForm) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ContainerRemove", arg0, arg1) + ret := m.ctrl.Call(m, "ContainerRemove", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // ContainerRemove indicates an expected call of ContainerRemove. -func (mr *MockDockerMockRecorder) ContainerRemove(arg0, arg1 any) *gomock.Call { +func (mr *MockDockerMockRecorder) ContainerRemove(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerRemove", reflect.TypeOf((*MockDocker)(nil).ContainerRemove), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerRemove", reflect.TypeOf((*MockDocker)(nil).ContainerRemove), arg0, arg1, arg2) } // ContainerRename mocks base method. -func (m *MockDocker) ContainerRename(arg0, arg1 string) error { +func (m *MockDocker) ContainerRename(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ContainerRename", arg0, arg1) + ret := m.ctrl.Call(m, "ContainerRename", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // ContainerRename indicates an expected call of ContainerRename. -func (mr *MockDockerMockRecorder) ContainerRename(arg0, arg1 any) *gomock.Call { +func (mr *MockDockerMockRecorder) ContainerRename(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerRename", reflect.TypeOf((*MockDocker)(nil).ContainerRename), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerRename", reflect.TypeOf((*MockDocker)(nil).ContainerRename), arg0, arg1, arg2) } // ContainerStart mocks base method. -func (m *MockDocker) ContainerStart(arg0 string) error { +func (m *MockDocker) ContainerStart(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ContainerStart", arg0) + ret := m.ctrl.Call(m, "ContainerStart", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // ContainerStart indicates an expected call of ContainerStart. -func (mr *MockDockerMockRecorder) ContainerStart(arg0 any) *gomock.Call { +func (mr *MockDockerMockRecorder) ContainerStart(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStart", reflect.TypeOf((*MockDocker)(nil).ContainerStart), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStart", reflect.TypeOf((*MockDocker)(nil).ContainerStart), arg0, arg1) } // ContainerStats mocks base method. -func (m *MockDocker) ContainerStats(arg0 string) (io.ReadCloser, error) { +func (m *MockDocker) ContainerStats(arg0 context.Context, arg1 string) (io.ReadCloser, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ContainerStats", arg0) + ret := m.ctrl.Call(m, "ContainerStats", arg0, arg1) ret0, _ := ret[0].(io.ReadCloser) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerStats indicates an expected call of ContainerStats. -func (mr *MockDockerMockRecorder) ContainerStats(arg0 any) *gomock.Call { +func (mr *MockDockerMockRecorder) ContainerStats(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStats", reflect.TypeOf((*MockDocker)(nil).ContainerStats), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStats", reflect.TypeOf((*MockDocker)(nil).ContainerStats), arg0, arg1) } // ContainerStop mocks base method. -func (m *MockDocker) ContainerStop(arg0 string) error { +func (m *MockDocker) ContainerStop(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ContainerStop", arg0) + ret := m.ctrl.Call(m, "ContainerStop", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // ContainerStop indicates an expected call of ContainerStop. -func (mr *MockDockerMockRecorder) ContainerStop(arg0 any) *gomock.Call { +func (mr *MockDockerMockRecorder) ContainerStop(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStop", reflect.TypeOf((*MockDocker)(nil).ContainerStop), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStop", reflect.TypeOf((*MockDocker)(nil).ContainerStop), arg0, arg1) } // ContainersList mocks base method. -func (m *MockDocker) ContainersList(arg0 context.Context, arg1 filters.Args) []*entities.Container { +func (m *MockDocker) ContainersList(arg0 context.Context, arg1 filters.Args) ([]*entities.Container, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainersList", arg0, arg1) ret0, _ := ret[0].([]*entities.Container) - return ret0 + ret1, _ := ret[1].(error) + return ret0, ret1 } // ContainersList indicates an expected call of ContainersList. @@ -142,18 +143,18 @@ func (mr *MockDockerMockRecorder) ContainersList(arg0, arg1 any) *gomock.Call { } // GetContainer mocks base method. -func (m *MockDocker) GetContainer(arg0 string) (*entities.Container, error) { +func (m *MockDocker) GetContainer(arg0 context.Context, arg1 string) (*entities.Container, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetContainer", arg0) + ret := m.ctrl.Call(m, "GetContainer", arg0, arg1) ret0, _ := ret[0].(*entities.Container) ret1, _ := ret[1].(error) return ret0, ret1 } // GetContainer indicates an expected call of GetContainer. -func (mr *MockDockerMockRecorder) GetContainer(arg0 any) *gomock.Call { +func (mr *MockDockerMockRecorder) GetContainer(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContainer", reflect.TypeOf((*MockDocker)(nil).GetContainer), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContainer", reflect.TypeOf((*MockDocker)(nil).GetContainer), arg0, arg1) } // ImageRemove mocks base method. @@ -185,15 +186,16 @@ func (mr *MockDockerMockRecorder) ImageTag(arg0, arg1, arg2 any) *gomock.Call { } // ImagesList mocks base method. -func (m *MockDocker) ImagesList() []*entities.Image { +func (m *MockDocker) ImagesList(arg0 context.Context, arg1 filters.Args) ([]*entities.Image, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ImagesList") + ret := m.ctrl.Call(m, "ImagesList", arg0, arg1) ret0, _ := ret[0].([]*entities.Image) - return ret0 + ret1, _ := ret[1].(error) + return ret0, ret1 } // ImagesList indicates an expected call of ImagesList. -func (mr *MockDockerMockRecorder) ImagesList() *gomock.Call { +func (mr *MockDockerMockRecorder) ImagesList(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImagesList", reflect.TypeOf((*MockDocker)(nil).ImagesList)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImagesList", reflect.TypeOf((*MockDocker)(nil).ImagesList), arg0, arg1) } diff --git a/telegram/keyboards/containers.go b/telegram/keyboards/containers.go index f64e8db..d0898f6 100644 --- a/telegram/keyboards/containers.go +++ b/telegram/keyboards/containers.go @@ -3,12 +3,14 @@ package keyboards import ( "fmt" + "github.com/arshamalh/dockeroller/entities" "github.com/arshamalh/dockeroller/telegram/btns" "gopkg.in/telebot.v3" ) -func ContainersList(index int, containerIsOn bool) *telebot.ReplyMarkup { +func ContainersList(containerID string, index int, containerIsOn bool) *telebot.ReplyMarkup { keyboard := &telebot.ReplyMarkup{} + containerID = containerID[:entities.LEN_CONT_TRIM] keyboard.Inline( telebot.Row{ @@ -16,37 +18,53 @@ func ContainersList(index int, containerIsOn bool) *telebot.ReplyMarkup { keyboard.Data("Next ➡", btns.ContNext.String(), fmt.Sprint(index+1)), }, telebot.Row{ - switchBtn(keyboard, index, containerIsOn), - keyboard.Data("Remove 🗑", btns.ContRemoveForm.String(), fmt.Sprint(index)), - keyboard.Data("Rename ✏️", btns.ContRename.String(), fmt.Sprint(index)), + switchBtn(keyboard, containerID, containerIsOn), + keyboard.Data("Remove 🗑", btns.ContRemoveForm.String(), containerID, fmt.Sprint(index)), + keyboard.Data("Rename ✏️", btns.ContRename.String(), containerID, fmt.Sprint(index)), }, telebot.Row{ - keyboard.Data("Logs 🪵", btns.ContLogs.String(), fmt.Sprint(index)), - keyboard.Data("Stats 📊", btns.ContStats.String(), fmt.Sprint(index)), + keyboard.Data("Logs 🪵", btns.ContLogs.String(), containerID, fmt.Sprint(index)), + keyboard.Data("Stats 📊", btns.ContStats.String(), containerID, fmt.Sprint(index)), }, ) return keyboard } -func ContainerRemove(index int, force, removeVolumes bool) *telebot.ReplyMarkup { +func ContainerRemove(containerID string, index int, force, removeVolumes bool) *telebot.ReplyMarkup { keyboard := &telebot.ReplyMarkup{} + containerID = containerID[:entities.LEN_CONT_TRIM] keyboard.Inline( telebot.Row{ keyboard.Data( fmt.Sprintf("Force: %t", force), string(btns.ContRemoveForce), + containerID, fmt.Sprint(index), ), keyboard.Data( fmt.Sprintf("Remove Volumes: %t", removeVolumes), string(btns.ContRemoveVolume), + containerID, fmt.Sprint(index), ), }, telebot.Row{ - keyboard.Data("Done", string(btns.ContRemoveDone), fmt.Sprint(index)), + keyboard.Data( + "Done", + string(btns.ContRemoveDone), + containerID, + fmt.Sprint(index), + ), + }, + telebot.Row{ + keyboard.Data( + "Back ⬅", + btns.ContBack.String(), + containerID, + fmt.Sprint(index), + ), }, ) @@ -57,17 +75,17 @@ func ContainerBack(index int) *telebot.ReplyMarkup { keyboard := &telebot.ReplyMarkup{} keyboard.Inline( telebot.Row{ - keyboard.Data("Back ⬅", btns.ContBack.String(), fmt.Sprint(index)), + keyboard.Data("Back ⬅", btns.ContBack.String()), }, ) return keyboard } -func switchBtn(keyboard *telebot.ReplyMarkup, index int, containerIsOn bool) telebot.Btn { +func switchBtn(keyboard *telebot.ReplyMarkup, containerID string, containerIsOn bool) telebot.Btn { if containerIsOn { - return keyboard.Data("Stop 🛑", btns.ContStop.String(), fmt.Sprint(index)) + return keyboard.Data("Stop 🛑", btns.ContStop.String(), containerID) } else { - return keyboard.Data("Start 🏃", btns.ContStart.String(), fmt.Sprint(index)) + return keyboard.Data("Start 🏃", btns.ContStart.String(), containerID) } } diff --git a/telegram/keyboards/images.go b/telegram/keyboards/images.go index 066e114..41704f3 100644 --- a/telegram/keyboards/images.go +++ b/telegram/keyboards/images.go @@ -3,6 +3,7 @@ package keyboards import ( "fmt" + "github.com/arshamalh/dockeroller/entities" "github.com/arshamalh/dockeroller/telegram/btns" "gopkg.in/telebot.v3" ) @@ -26,24 +27,28 @@ func ImagesList(index int) *telebot.ReplyMarkup { return keyboard } -func ImageRemove(index int, force, pruneChildren bool) *telebot.ReplyMarkup { +func ImageRemove(imageID string, force, pruneChildren bool) *telebot.ReplyMarkup { keyboard := &telebot.ReplyMarkup{} + imageID = imageID[:entities.LEN_IMG_TRIM] keyboard.Inline( telebot.Row{ keyboard.Data( fmt.Sprintf("Force: %t", force), string(btns.ImgRmForce), - fmt.Sprint(index), + imageID, ), keyboard.Data( fmt.Sprintf("Prune Children: %t", pruneChildren), string(btns.ImgRmPruneCh), - fmt.Sprint(index), + imageID, ), }, telebot.Row{ - keyboard.Data("Done", string(btns.ImgRmDone), fmt.Sprint(index)), + keyboard.Data("Done", string(btns.ImgRmDone), imageID), + }, + telebot.Row{ + keyboard.Data("Back ⬅", string(btns.ImgBack)), }, ) diff --git a/telegram/middlewares/log.go b/telegram/middlewares/log.go index 8a97a7a..8a9b661 100644 --- a/telegram/middlewares/log.go +++ b/telegram/middlewares/log.go @@ -1,6 +1,7 @@ package middlewares import ( + "github.com/arshamalh/dockeroller/log" "go.uber.org/zap" "gopkg.in/telebot.v3" ) @@ -8,8 +9,11 @@ import ( func LoggerMiddleware(zapLogger *zap.Logger) func(next telebot.HandlerFunc) telebot.HandlerFunc { return func(next telebot.HandlerFunc) telebot.HandlerFunc { return func(ctx telebot.Context) error { - zapLogger.Info(ctx.Update().Message.Text) - return next(ctx) + if err := next(ctx); err != nil { + log.Gl.Error(err.Error()) + return err + } + return nil } } } diff --git a/telegram/msgs/callback_responses.go b/telegram/msgs/callback_responses.go index 1379200..ba8ea6f 100644 --- a/telegram/msgs/callback_responses.go +++ b/telegram/msgs/callback_responses.go @@ -1,21 +1,37 @@ package msgs -import "gopkg.in/telebot.v3" +import ( + "fmt" + + "gopkg.in/telebot.v3" +) var ( CannotStartTheContainer = NewCallbackResponse("We cannot start the container!") CannotStopTheContainer = NewCallbackResponse("We cannot stop the container!") - FillTheFormAndPressDone = NewCallbackResponse("Please fill the form and press done") + UnableToRemoveImage = NewCallbackResponse("Unable to remove image") UnableToRemoveContainer = NewCallbackResponse("Unable to remove container") - ContainerRemovedSuccessfully = NewCallbackResponse("Container removed successfully") + UnableToFetchContainers = NewCallbackResponse("Unable to fetch containers") + UnableToFetchImages = NewCallbackResponse("Unable to fetch images") + FillTheFormAndPressDone = NewCallbackResponse("Please fill the form and press done") InvalidButton = NewCallbackResponse("Invalid button 🤔️️️️️️") - UnableToRemoveImage = NewCallbackResponse("Unable to remove image") + ContainerRemovedSuccessfully = NewCallbackResponse("Container removed successfully") ImageRemovedSuccessfully = NewCallbackResponse("Image removed successfully") NoContainer = NewCallbackResponse("There is either no containers or you should run /containers again!") + NoImages = NewCallbackResponse("There is either no images or you should run /images again!") StartedButUnavailableCurrentState = NewCallbackResponse("Container started, but we're not able to show current state.") StoppedButUnavailableCurrentState = NewCallbackResponse("Container stopped, but we're not able to show current state.") + NoLogsAvailable = NewCallbackResponse("No logs available") + NoStatsAvailable = NewCallbackResponse("No stats available") + FinishingTheLogsStream = NewCallbackResponse("Finishing the logs stream") + FinishingTheStatsStream = NewCallbackResponse("Finishing the stats stream") ) func NewCallbackResponse(text string) *telebot.CallbackResponse { return &telebot.CallbackResponse{Text: text} } + +func FormattedCBResponse(format string, args ...any) *telebot.CallbackResponse { + msg := fmt.Sprintf(format, args...) + return NewCallbackResponse(msg) +} diff --git a/telegram/msgs/format.go b/telegram/msgs/format.go index 9f3054d..7daeb94 100644 --- a/telegram/msgs/format.go +++ b/telegram/msgs/format.go @@ -10,7 +10,7 @@ import ( // Monospace font is enabled by ` charater that is not supported in Go multiline string literals // So we should format it using ” and replacing them. -// characters ()_-.>=< are also reserved by telegram and we will replace them with their escaped ones. +// characters ()_-.><{}=+ are also reserved by telegram and we will replace them with their escaped ones. func FmtMono(input string) string { return strings.NewReplacer( "''", "`", @@ -22,6 +22,9 @@ func FmtMono(input string) string { "=", "\\=", "<", "\\<", ">", "\\>", + "+", "\\+", + "{", "\\{", + "}", "\\}", ).Replace(input) } @@ -37,9 +40,9 @@ func FmtContainer(container *entities.Container) string { func FmtImage(image *entities.Image) string { // In docker the result of `docker image -q` give us - // images ids with 12 characters long + // images ids with LEN_IMG_TRIM (=12) characters long imageIDTrimmed := strings.TrimPrefix(image.ID, "sha256:") - imageIDPart1 := imageIDTrimmed[0:12] + imageIDPart1 := imageIDTrimmed[0:entities.LEN_IMG_TRIM] imageIDPart2 := imageIDTrimmed[len(imageIDTrimmed)-4:] response := strings.NewReplacer( "{id1}", imageIDPart1, diff --git a/telegram/msgs/msgs.go b/telegram/msgs/msgs.go index 27e8905..b947ebf 100644 --- a/telegram/msgs/msgs.go +++ b/telegram/msgs/msgs.go @@ -22,6 +22,12 @@ Enter new container name: ImageNewNameInput = ` Enter new image tag: +` + + InvalidImageTag = ` +invalid tag, tags should be: +- all lower case +- with no space ` Container = ` @@ -39,9 +45,9 @@ Enter new image tag: ` Stat = ` -''CPU Usage: '' {cpu_usage} % -''Online CPUs: '' {online_cpus} -''Memory Usage: '' {memory_usage} ({memory_usage%} %) -''Available Mem: '' {available_memory} +''CPU Usage: ''{cpu_usage} % +''Online CPUs: ''{online_cpus} +''Memory Usage: ''{memory_usage} ({memory_usage%} %) +''Available Mem: ''{available_memory} ` ) diff --git a/tools/extractID_and_data.go b/tools/extractID_and_data.go new file mode 100644 index 0000000..ab4cb55 --- /dev/null +++ b/tools/extractID_and_data.go @@ -0,0 +1,10 @@ +package tools + +import "strings" + +func ExtractIndexAndID(contextData string) (ID string, index int) { + rawData := strings.Split(contextData, "|") + ID = rawData[0] + index = Str2Int(rawData[1]) + return ID, index +} diff --git a/tools/str2int.go b/tools/str2int.go new file mode 100644 index 0000000..479ff7a --- /dev/null +++ b/tools/str2int.go @@ -0,0 +1,17 @@ +package tools + +import ( + "strconv" + + "github.com/arshamalh/dockeroller/log" +) + +// We are sure that the received index is conversable to int, +// so we simplify the error handling by just logging it (for worst case) +func Str2Int(index string) int { + int, err := strconv.Atoi(index) + if err != nil { + log.Gl.Error(err.Error()) + } + return int +}