Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use the testing server? #105

Closed
gravis opened this issue Jun 2, 2014 · 33 comments
Closed

How to use the testing server? #105

gravis opened this issue Jun 2, 2014 · 33 comments

Comments

@gravis
Copy link

gravis commented Jun 2, 2014

I'm a bit lost with https://github.com/fsouza/go-dockerclient/blob/master/testing/server.go
I though I would be able to use it as a fake local server, but most of the funcs are private. Therefore, if I "wait" for a container in my tests, I don't have anything to change the running state to false, and exit of the for loop here:
https://github.com/fsouza/go-dockerclient/blob/master/testing/server.go#L358
Maybe I'm missing something?

Thanks

@fsouza
Copy link
Owner

fsouza commented Jun 3, 2014

Hi @gravis, sorry about the delay.

The testing server is intended to be used in the same place as an actual server, so you can point a Client instance to it. Something like:

server, err := testing.NewServer("127.0.0.1:0", nil)
// ...
client, err := docker.NewClient(server.URL())
// use the client

@gravis
Copy link
Author

gravis commented Jun 3, 2014

Ok, but how do you use the wait command then? The container is stuck
until timeout. :(
Thanks

@fsouza
Copy link
Owner

fsouza commented Jun 3, 2014

You would need to call Stop + Wait. I think we have something like this in tsuru, please hold on.

@fsouza
Copy link
Owner

fsouza commented Jun 3, 2014

Just confirmed: we have a gorountine that stops the container and automatically "unblocks" the WaitContainer call.

@gravis
Copy link
Author

gravis commented Jun 4, 2014

Ok, thanks. It would be nice if you could share some code here, I'm still very confused.

I'm trying to test a command (ie: entrypoint) inside a container, in pseudo code, it's something like:

CreateContainer
StartContainer
WaitContainer
CopyFilesFromContainer
RemoveContainer

If I run a go routine to stop the container, how can I have hands on the resulting files inside the container? AFAIU, stopping the container will just emulate a timeout :(

Thanks

@fsouza
Copy link
Owner

fsouza commented Jun 4, 2014

I see. You will need to manually stop the container anyway, don't you? I mean, if methods weren't private in the testing server, how would you write your test?

Instead of using a goroutine, you could stop the container at any time, the time that you would access the testing server and mark the container as stopped, unleashing the WaitContainer call.

@gravis
Copy link
Author

gravis commented Jun 5, 2014

Currently, I'm creating a real http server, with methods like:

mux.HandleFunc(fmt.Sprintf("/containers/%s/attach", container_id), func(w http.ResponseWriter, r *http.Request) {
        outStream := utils.NewStdWriter(w, utils.Stdout)

        fmt.Fprintf(outStream, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n")
        fmt.Fprint(outStream, requires_txt)
    })

Maybe being able to pass my own handlers would be enough.
Otherwise, I would need:

  • A func to get the list of running containers (I should have only one)-
  • A func to stop the container (currently private func)
  • A func to set the output of a container (attach a io.Reader to all new created containers?)

What do you think?

@fsouza
Copy link
Owner

fsouza commented Jun 5, 2014

Hmm, this is interesting. Do you thing that making the server extensible, so you can specify the handler for an operation is enough?

What about the API? Something like:

Server.CustomHandler(path string, handler http.Handler)

Should the path be a regular expression or the actual path?

In your case, it would be:

server.CustomHandler(fmt.Sprintf("/containers/%s/attach", container_id), func(w http.ResponseWriter, r *http.Request) {
        outStream := utils.NewStdWriter(w, utils.Stdout)

        fmt.Fprintf(outStream, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n")
        fmt.Fprint(outStream, requires_txt)
    })

What do you think?

@gravis
Copy link
Author

gravis commented Jun 6, 2014

Sounds like a terrific idea. I still need a func to get the container_id btw.
What would be more awesome is to pass something like chan *docker.Container (buffered) to NewServer, and then send the container on the channel when created. I would be able to wait exactly the time needed, and get the container_id directly :)

But I think we're heading in the right direction, and I really appreciate your open mind :)

@gravis
Copy link
Author

gravis commented Jun 6, 2014

What about:

http://play.golang.org/p/7dLcZ5ksuW

:)

@fsouza
Copy link
Owner

fsouza commented Jun 6, 2014

Nice snippet, cChan should be exported, right?

Also, the channel should be fed when the client calls CreateContainer, and not on NewServer.

What do you think? Sounds fair?

@gravis
Copy link
Author

gravis commented Jun 6, 2014

Sure, it was a proof of concept. I'm very glad you like it
Indeed, cChan should be exported, and the container sent on create, not on server creation :)
We mustn't forget to close the chan when server.Close() is called btw.

Thanks!

@fsouza
Copy link
Owner

fsouza commented Jun 27, 2014

Sorry for the huge delay. I'm planning to do it, but can you confirm that this flow will be enough for your use case?

@gravis
Copy link
Author

gravis commented Jun 29, 2014

No problem at all. Yes, I think this would be really make it. If I find blocking issues, I will propose PRs anymay :)

Thanks a lot

@fsouza
Copy link
Owner

fsouza commented Jun 29, 2014

Can you take a look at #113? Thanks!

fsouza added a commit that referenced this issue Jul 2, 2014
fsouza added a commit that referenced this issue Jul 2, 2014
@fsouza
Copy link
Owner

fsouza commented Jul 4, 2014

I've just pushed the CustomHandler method, can you take a look? :) Is it good?

@gravis
Copy link
Author

gravis commented Jul 4, 2014

Damned, you're fast :)
Will take a look this week-end, I promise!

@fsouza
Copy link
Owner

fsouza commented Jul 5, 2014

No hurry man :) I was wondering: should we add a method to remove the custom handler?

@gravis
Copy link
Author

gravis commented Jul 8, 2014

Sorry, I still need some time to test this. I'm fighting a nasty bug with AttachContainer, hitting 100% CPU when getting container logs, and blocking all my routines :(

@fsouza
Copy link
Owner

fsouza commented Jul 8, 2014

No problem. Is it the testing server or the real server? Is the client or the server hitting 100% CPU? I may help debugging it, if you have a reproducer. :)

@gravis
Copy link
Author

gravis commented Jul 8, 2014

I have opened a ticket actually: #114
It's painfull to debug, because it occurs very randomly :(

@fsouza
Copy link
Owner

fsouza commented Jul 8, 2014

@gravis thanks for reporting it!

@gravis
Copy link
Author

gravis commented Jul 8, 2014

I'm wondering if it's the good approach now. I have started to replace my fake docker with testing.DockerServer, and ended up with copying/pasting the content of https://github.com/fsouza/go-dockerclient/blob/master/testing/server.go#L397, just to be able to replace the 2 last lines...

There's probably something else to explore. Maybe a CustomAttachOutput, instead of overriding the whole container method?

@fsouza
Copy link
Owner

fsouza commented Jul 9, 2014

@gravis what about something like SetContainerOutput?

@gravis
Copy link
Author

gravis commented Jul 9, 2014

sounds good! Sorry for changing target like that. It's only when you have hands in the code that you realize how the API should behave... :(

@gravis
Copy link
Author

gravis commented Jul 9, 2014

I also have some files I need to write in the container, I don't think there a method to add files in a container ? If not, I will probably need also something like: AddFileToContainer(r io.Reader, path string) error

@fsouza
Copy link
Owner

fsouza commented Jul 9, 2014

@gravis how would you access these files?

Currently, "go-dockerclient" doesn't support copying files from the container. Wouldn't you need a CopyFiles method as well?

@gravis
Copy link
Author

gravis commented Jul 9, 2014

From the container? You mean, TO the container, right? If it is TO the container, yes, it will be needed :)

@fsouza
Copy link
Owner

fsouza commented Jul 9, 2014

Sorry, I meant from the container, but I think I'm misunderstanding your use case.

Will you insert files in the container in the testing server? What's the purpose? I mean, the test code would look like:

runTheServer()
insertFileInTheContainer()
callTheProductionCode()

What does the production code do?

@gravis
Copy link
Author

gravis commented Jul 9, 2014

Ok, to sum up. We have containers to run isolated processes. Theses commands will generate some output (container logs), and some files to be copied from the container, and sent to another API.
To be able to test, I currently create a fake server (see below). It would be an improvement to use something closer to docker, like the testing server.

requires_txt := "Flask==0.8\nJinja2==2.6"

    mux := http.NewServeMux()

    // CreateContainer stub
    container_id := "5fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
    mux.HandleFunc(fmt.Sprintf("/containers/create"), func(w http.ResponseWriter, r *http.Request) {
        jsonContainer := fmt.Sprintf(`{
    "Id": "%s",
    "Warnings": []
    }`, container_id)
        fmt.Fprint(w, jsonContainer)
    })

    // StartContainer stub
    mux.HandleFunc(fmt.Sprintf("/containers/%s/start", container_id), func(w http.ResponseWriter, r *http.Request) {})

    // WaitContainer stub
    mux.HandleFunc(fmt.Sprintf("/containers/%s/wait", container_id), func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, `{"StatusCode": 0}`)
    })

    mux.HandleFunc(fmt.Sprintf("/containers/%s/attach", container_id), func(w http.ResponseWriter, r *http.Request) {
        outStream := utils.NewStdWriter(w, utils.Stdout)

        fmt.Fprintf(outStream, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n")
        fmt.Fprint(outStream, requires_txt)
    })

    // CopyFromContainer stub
    mux.HandleFunc(fmt.Sprintf("/containers/%s/copy", container_id), func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/octet-stream")
        // Create a buffer to write our archive to.
        buf := new(bytes.Buffer)

        // Create a new tar archive.
        tw := tar.NewWriter(buf)

        // Add some files to the archive.
        var files = []struct {
            Name, Body string
        }{
            {"requires.txt", requires_txt},
        }
        for _, file := range files {
            hdr := &tar.Header{
                Name: file.Name,
                Size: int64(len(file.Body)),
            }
            if err := tw.WriteHeader(hdr); err != nil {
                log.Fatal(err)
            }
            if _, err := tw.Write([]byte(file.Body)); err != nil {
                log.Fatal(err)
            }
        }
        // Make sure to check the error on Close.
        if err := tw.Close(); err != nil {
            log.Fatal(err)
        }
        if _, err := io.Copy(w, buf); err != nil {
            // return err
        }
    })
    server := httptest.NewServer(mux)

@fsouza
Copy link
Owner

fsouza commented Jul 9, 2014

I understand now. In order to insert files into the container or customise the output, the container would have to exist, so you'd need to be able to call the methods SetContainerOutput and AddFileToContainer after the CreateContainer call, but before the Logs/Attach and CopyFromContainer calls.

Am I understanding it right? I think this is a scenario that is quite hard to test.

@gravis
Copy link
Author

gravis commented Jul 10, 2014

You're 100% right :)

@fsouza
Copy link
Owner

fsouza commented Jul 10, 2014

I don't think you would be able to guarantee such order. You'd need some mechanism to "pre-register" the container, and have CreateContainer returning that container. Something like:

container, err := server.PrepareContainerForCreation()
server.SetContainerOutput(container, "something")
server.AddFileToTheContainer(container, "/filename", "file content")
server.AddFileToTheContainer(container, "/etc/filename", "file content")
server.AddFileToTheContainer(container, "/tmp/filename", "file content")
<call production code>
<do assertions>

Then the production code will call CreateContainer, StartContainer, WaitContainer, AttachToContainer and CopyFromContainer, using the container returned by CreateContainer, that is the container previously prepared for creation. This would enable us to prepare the behaviour of a container before creating it. Also, as far as I can tell, we would not need the custom handler anymore.

What do you think?

@fsouza fsouza closed this as completed Mar 3, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants