Hi! Dagger looks really cool and I am contemplating doing a PoC with it. But I have a hard requirement to implement “wait for user input” functionality (manually approving or rejecting deployments).
I can’t seem to figure out how to achieve such a thing using Dagger.
How would you go about this requirement? Are there any resources on this subject?
#Wait for user input
1 messages · Page 1 of 1 (latest)
I don't think ive seen an example about Authorization of a command, though I have thought of this myself a few times now. I did not think its much of an issue though, in most cases, you will always have a remote CI Environment, this CI environment is generally where priveleges, user groups, triggers of a pipeline will occur.
So you can still do a POC to remove all the execution of the 'happening' but still have some scripting or wrapping commands which 'control' when dagger is executed.
Some of that may be CI Platform specific controls. If you're thinking of a purely local solution, I think you would 'design the pipelines to be executed' in dagger, and then have some control around how the dagger pipeline is executed, that would be shell or powershell or something like this.
Also maybe there is opportunity on the very recently introduced Terminal() feature, which gives you an interactive terminal into a container, but its core feature is really for debugging. I had a quick look, thinking you could possibly have some interactivity, create 1 container with a spawned terminal which has a prompt to the user, depending on the exit code of that container, either do something or return. Didn't manage to get that working.
you totally could have a Terminal, with a custom little shell that requires you to type either approve/deny, and that writes a file on the filesystem - then you can check if that was written or not
you could even encapsulate this behavior into a module potentially to be able to re-use it?
there's sadly not a way to do this in the core api, i can definitely see the value for it - but i guess i'm struggling to understand how this would integrate in a ci environment (where no Terminal api is available)
^ this. Here's a workaround example where you can store the WithExec output in a cache volume file and read from that afterwards.
func (m *Lele) Test() {
opt, _ := dag.Container().
From("alpine").
WithMountedCache("/tmp/replies", dag.CacheVolume("replies")).
Terminal(dagger.ContainerTerminalOpts{Cmd: []string{"sh", "-c", `read -p "Pick one (Red/Blue): " && echo -n $REPLY > /tmp/replies/pill`}}).WithExec([]string{"cat", "/tmp/replies/pill"}).Stdout(context.Background())
fmt.Println("The selected option was", opt)
}
having said that, it'd be nice to do something like Terminal().Stdout to get the output directly from the terminal command which is not currently possible instead of doing all this cachevolume dancing
you don't even need the cache volume dance i don't think
you can just do it with a normal volume, write the file, read the file, unmount the volume
Super common with certain tools (often deployment or hybrid CI/CD tools like AWS Codepipeline) to use that tool to do the manual 👍👎
@pale remnant what build, CI, and deployment tooling are you looking to try Dagger with?
WDYTM with a "normal volume"?
You can just to withDirectory
oh, tried that also but Terminal doesn't commit the changes to the underlying container FS (not sure how that works internally). So using WithDirectory didn't work for me.
i.e:
d := dag.Directory()
dag.Container().
From("alpine").
//WithMountedCache("/tmp/replies", dag.CacheVolume("replies")).
WithDirectory("/tmp/replies", d).
Terminal(dagger.ContainerTerminalOpts{Cmd: []string{"sh", "-c", `read -p "Pick one (Red/Blue): " && echo -n $REPLY > /tmp/replies/pill`}}).WithExec([]string{"cat", "/tmp/replies/pill"}).Stdout(context.Background())
fmt.Println(d.Entries(context.Background()))
what build, CI, and deployment tooling are you looking to try Dagger with?
Currently using Maven and Docker, Jenkins and Nomad.
Actually, there is a wish to migrate to GitHub Actions. But we have a huge amount of logic in Jenkins Groovy and I am not looking forward to migrate that all to a yaml based approach.
My hope is to start adopting Dagger icw Jenkins and later change Jenkins with GHA.
that explains why I couldn't get it working then
I think...
the docs don't demonstrate the terminal writing anything back and it does strongly mention debugging. if we confirm terminal is still valid maybe a cookbook example could be added. though I also would like to know what happens with that in a CI platform.
it will fail since a tty can't be allocated in a CI platform
simulateTerminal and feed it input lines to execute on each terminal creation. (lol I'm joking)
but in all seriousness it could be possible to kind of follow how some Ci platforms allow tty St various stages. seems like it could get really messy though
sorry not tty. I mean like form inputs like gitlab has a form with arguments. that's not really the same thing I suppose.
@brave flicker so cache is required when using terminal if you need to write back? when I am back I want to fix my broken sample which tried to do take user input
yes, the only way to get something back of the Terminal command is using cache volumes currently.
@pale remnant I'm curious, is this currently supposted in your current CI / workflows? What's the current UX to achieve this?
i think actually that this is exactly the same internally as how we do services
it would be nice if we could writeback 🙂
instead of relying on cache volumes, which feels a bit yucky
yes, I spoke with Andrea earlier and he told me Terminal is actually a service 😄
I cant get this example to work based off the cache example, i thought this would be a nice way to control it, but somehow deploy is always true. probably i am doing something wrong
package main
import (
"context"
"dagger/fakedeploy/internal/dagger"
"fmt"
"strings"
"time"
)
type Fakedeploy struct {
approved bool
}
func (m *Fakedeploy) WithApproval() (*Fakedeploy, error) {
response, _ := dag.Container().
From("alpine").
WithMountedCache("/tmp/replies", dag.CacheVolume(fmt.Sprintf("reply-%d", time.Now()))).
Terminal(dagger.ContainerTerminalOpts{
Cmd: []string{
"sh",
"-c",
"read -p 'Deploy: ' && echo $REPLY > /tmp/replies/reply"},
}).
WithExec([]string{"cat", "/tmp/replies/reply"}).
Stdout(context.Background())
m.approved = strings.Contains(response, "yes")
return m, nil
}
func (m *Fakedeploy) Deploy() (string, error) {
if m.approved == true {
return dag.Container().
From("ubuntu:latest").
WithExec([]string{"echo", "Deploying..."}).
WithExec([]string{"sleep", "5"}).
WithExec([]string{"echo", "Deployed"}).Stdout(context.Background())
} else {
return "", fmt.Errorf("deployment not approved")
}
}
dagger call with-approval deploy
thought maybe it was do to with the cache, so i added a time to it but still no luck
@vital shore approved needs to be public for this to work
huh
I know what's happening 😄
I am more confused now than ever

the cat is being cached sorry
I saw this in the log.
✔ start sh -c read -p 'Deploy: ' && echo $REPLY > /tmp/replies/reply 2.3s
✔ Container.withExec(args: ["cat", "/tmp/replies/reply"]): Container! 0.0s
✔ load cache: exec cat /tmp/replies/reply 0.0s
i could name the file with a date
yep
the next relese of Dagger has some TUI improvements to show cached operations better
i think the unintitive thing here is "we gotta use cache to grab the output" but then make sure the output is never cached
😄
haha
Okay got it working now. Simple 'approval' chaining which might be a little nicer to manage and also if you design any functions, you can always check the approval before executing.
package main
import (
"context"
"dagger/fakedeploy/internal/dagger"
"fmt"
"strings"
"time"
)
type Fakedeploy struct {
Approved bool
}
func (m *Fakedeploy) WithApproval() (*Fakedeploy, error) {
response, _ := dag.Container().
From("alpine").
WithMountedCache("/tmp/replies", dag.CacheVolume(fmt.Sprintf("reply-%d", time.Now()))).
Terminal(dagger.ContainerTerminalOpts{
Cmd: []string{
"sh",
"-c",
"read -p 'Deploy: ' && echo $REPLY > /tmp/replies/reply"},
}).
WithExec([]string{"echo", fmt.Sprintf("'cache buster %s'", time.Now())}).
WithExec([]string{"cat", "/tmp/replies/reply"}).
Stdout(context.Background())
m.Approved = strings.Contains(response, "yes")
return m, nil
}
func (m *Fakedeploy) Deploy() (string, error) {
if m.Approved == true {
return dag.Container().
From("ubuntu:latest").
WithExec([]string{"echo", "Deploying..."}).
WithExec([]string{"sleep", "5"}).
WithExec([]string{"echo", "Deployed"}).Stdout(context.Background())
} else {
return "deployment not approved", nil
}
}
dagger call deploy should fail, since the approval was not set to true by using the with-approval
dagger call with-approval deploy should fail, if the response is not 'yes' and should pass, if the response contains yes.
Since this is such a critical topic too (approvals in general, especially large companies) maybe we can add this for a larger discussion or add something on github about the DX and gate-keeper type of patterns that can be achieved with dagger?
yes, SGTM! We can create an issue in the dagger/dagger repo and continue there 🙏
@pale remnant maybe the above could satisfy your requirement?
Think I'll add a second issue around Terminal documentation (maybe not leaning enough into its usefuless outside of debugging
Late to this thread, but are you using an existing CI system that has this capability?
I've been thinking about this a bit with CircleCI for example that has a wait-for step in their pipeline and the way that I was thinking about solving this was to have it work like this
- Dagger Call function A
- CircleCI Wait for Input
- Dagger Call function B
So instead of trying to solve this inside of dagger, I would be taking advantage of the built in tooling in the CI system
Are these wait-for things the famous "approval" jobs in Cirlce? Or is there something else? I haven't seen any other feature regarding that.
In any case this could also be very easily achieved by implement it in userspace via polling a URL or something like that.
Yeah that is what I mean, the "approval job"
dag.Http or Exec curl - http:/some/internal/company/service/approve (post req) (could serialise some details about the dagger run?) (wait for HTTP Response 200) - user 'approves' on the other end outside of the dagger pipeline.
yep, correct. Pretty much like any 2fa flow
It looks like this (picture found on internet, in our case stages and wording is a bit different, but the idea is the same).
Is that a Jenkins plugin?
Yeah maybe, I’ll try it out soon.
This does not seem to implement a timeout after a period of waiting, right?
Maybe? I can check tomorrow. I’ve seen it used so many times that I consider it native to Jenkins.
I consider it an anti-pattern. But in some industries there are requirements for a manual stamp of approval.