From 939006ff20bbb5b1c57760b0625342a52266e4aa Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Mon, 20 Apr 2015 17:15:58 +0200 Subject: [PATCH] functional tests: Import test-aci-auth-server It is now known as test-auth-server and it contains an aci subpackage. Later we can add a docker package to test docker auth. --- tests/test-auth-server/README.md | 85 +++++++++ tests/test-auth-server/aci/aci.go | 135 +++++++++++++ tests/test-auth-server/aci/commands.go | 37 ++++ tests/test-auth-server/aci/server.go | 255 +++++++++++++++++++++++++ tests/test-auth-server/main.go | 101 ++++++++++ 5 files changed, 613 insertions(+) create mode 100644 tests/test-auth-server/README.md create mode 100644 tests/test-auth-server/aci/aci.go create mode 100644 tests/test-auth-server/aci/commands.go create mode 100644 tests/test-auth-server/aci/server.go create mode 100644 tests/test-auth-server/main.go diff --git a/tests/test-auth-server/README.md b/tests/test-auth-server/README.md new file mode 100644 index 0000000..d1418b9 --- /dev/null +++ b/tests/test-auth-server/README.md @@ -0,0 +1,85 @@ +Before running `test-aci-auth-server` make sure that `go` and `actool` are in your +`$PATH`. + +``` +$ ./test-aci-auth-server start basic + +{ + "rktKind": "auth", + "rktVersion": "v1", + "domains": ["127.0.0.1:48608"], + "type": "basic", + "credentials": + { + "user": "bar", + "password": "baz" + } +} + +Ready, waiting for connections at https://127.0.0.1:48608 +``` + +(You can run `test-aci-auth-server start` with either `none`, `basic` or `oauth` parameter) + +Copy the snippet to `/etc/rkt/auth.d/test.json` and run `rkt +--insecure-skip-verify run +https://127.0.0.1:48608//prog.aci`. The `rkt` output ought +to be something like: +``` +# rkt --insecure-skip-verify run https://127.0.0.1:48608/basic1/prog.aci +rkt: fetching image from https://127.0.0.1:48608/basic1/prog.aci + + + +Authentication succeeded. +Sending SIGTERM to remaining processes... +Sending SIGKILL to remaining processes... +Unmounting file systems. +Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/pts. +Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/shm. +Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/sys. +Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/proc. +Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/console. +Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/tty. +Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/urandom. +Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/random. +Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/full. +Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/zero. +Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/null. +Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs. +Unmounting /proc/sys/kernel/random/boot_id. +Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs. +All filesystems unmounted. +Halting system. +``` + +While the additional output from `test-aci-auth-server`: +``` +Trying to serve "/basic10/prog.aci" + serving + done. +``` + +The `test-aci-auth-server start` with `oauth` will print something like this: +``` +$ ./test-aci-auth-server start oauth + +{ + "rktKind": "auth", + "rktVersion": "v1", + "domains": ["127.0.0.1:48805"], + "type": "oauth", + "credentials": + { + "token": "sometoken" + } +} + +Ready, waiting for connections at https://127.0.0.1:48805 +``` + +To stop the `test-aci-auth-server` run stop command: +``` +$ ./test-aci-auth-server stop https://127.0.0.1:60268 +Response status: 200 OK +``` diff --git a/tests/test-auth-server/aci/aci.go b/tests/test-auth-server/aci/aci.go new file mode 100644 index 0000000..776104c --- /dev/null +++ b/tests/test-auth-server/aci/aci.go @@ -0,0 +1,135 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aci + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "time" +) + +type aciToolkit struct { + acTool string + goTool string +} + +func (t *aciToolkit) prepareACI() ([]byte, error) { + dir, err := t.createTree() + if dir != "" { + defer os.RemoveAll(dir) + } + if err != nil { + return nil, fmt.Errorf("failed to build ACI tree: %v", err) + } + if err := t.buildProg(dir); err != nil { + return nil, fmt.Errorf("failed to build test program: %v", err) + } + fn, err := t.buildACI(dir) + if err != nil { + return nil, fmt.Errorf("failed to build ACI: %v", err) + } + defer os.Remove(fn) + contents, err := ioutil.ReadFile(fn) + if err != nil { + return nil, fmt.Errorf("failed to read ACI to memory: %v", err) + } + return contents, nil +} + +const ( + manifestStr = `{"acKind":"ImageManifest","acVersion":"0.5.1+git","name":"testprog","app":{"exec":["/prog"],"user":"0","group":"0"}}` + testProgSrcStr = ` +package main + +import "fmt" + +func main() { + fmt.Println("Authentication succeeded.") +} +` +) + +func (t *aciToolkit) createTree() (string, error) { + aciDir := "ACI" + rootDir := filepath.Join(aciDir, "rootfs") + manifestFile := filepath.Join(aciDir, "manifest") + srcFile := filepath.Join(rootDir, "prog.go") + if err := os.Mkdir(aciDir, 0755); err != nil { + return "", fmt.Errorf("failed to create ACI directory: %v", err) + } + if err := os.Mkdir(rootDir, 0755); err != nil { + return aciDir, fmt.Errorf("failed to create rootfs directory: %v", err) + } + if err := ioutil.WriteFile(manifestFile, []byte(manifestStr), 0644); err != nil { + return "", fmt.Errorf("failed to write manifest: %v", err) + } + if err := ioutil.WriteFile(srcFile, []byte(testProgSrcStr), 0644); err != nil { + return "", fmt.Errorf("failed to write go source: %v", err) + } + return aciDir, nil +} + +func (t *aciToolkit) buildProg(aciDir string) error { + args := []string{ + "go", + "build", + "-o", + "prog", + "./prog.go", + } + dir := filepath.Join(aciDir, "rootfs") + return runTool(t.goTool, args, dir) +} + +func (t *aciToolkit) buildACI(aciDir string) (string, error) { + timedata, err := time.Now().MarshalBinary() + if err != nil { + return "", fmt.Errorf("failed to serialize current date to bytes: %v", err) + } + if err := ioutil.WriteFile(filepath.Join(aciDir, "rootfs", "stamp"), timedata, 0644); err != nil { + return "", fmt.Errorf("failed to write a stamp: %v", err) + } + fn := "prog-build.aci" + args := []string{ + "actool", + "build", + aciDir, + fn, + } + if err := runTool(t.acTool, args, ""); err != nil { + return "", err + } + return fn, nil +} + +func runTool(tool string, args []string, dir string) error { + outBuf := new(bytes.Buffer) + errBuf := new(bytes.Buffer) + cmd := exec.Cmd{ + Path: tool, + Args: args, + Dir: dir, + Stdout: outBuf, + Stderr: errBuf, + } + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to execute `%s %s`: %v\nstdout:\n%v\n\nstderr:\n%v)", args[0], args[1], err, outBuf.String(), errBuf.String()) + } + return nil +} diff --git a/tests/test-auth-server/aci/commands.go b/tests/test-auth-server/aci/commands.go new file mode 100644 index 0000000..eef1488 --- /dev/null +++ b/tests/test-auth-server/aci/commands.go @@ -0,0 +1,37 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aci + +import ( + "crypto/tls" + "fmt" + "net/http" +) + +func StartServer(auth Type) (*Server, error) { + return NewServer(auth, 10) +} + +func StopServer(host string) (*http.Response, error) { + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: transport} + res, err := client.Post(host, "whatever", nil) + if err != nil { + return nil, fmt.Errorf("failed to send post to %q: %v", host, err) + } + return res, nil +} diff --git a/tests/test-auth-server/aci/server.go b/tests/test-auth-server/aci/server.go new file mode 100644 index 0000000..a037b91 --- /dev/null +++ b/tests/test-auth-server/aci/server.go @@ -0,0 +1,255 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aci + +import ( + "crypto/tls" + "encoding/base64" + "fmt" + "net/http" + "net/http/httptest" + "os/exec" + "path/filepath" + "strings" +) + +type Type int + +const ( + None Type = iota + Basic + Oauth +) + +type httpError struct { + code int + message string +} + +func (e *httpError) Error() string { + return fmt.Sprintf("%d: %s", e.code, e.message) +} + +type serverHandler struct { + auth Type + stop chan<- struct{} + msg chan<- string + tools *aciToolkit +} + +func (h *serverHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "POST": + w.WriteHeader(http.StatusOK) + h.stop <- struct{}{} + return + case "GET": + // handled later + default: + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + switch h.auth { + case None: + // no auth to do. + case Basic: + payload, httpErr := getAuthPayload(r, "Basic") + if httpErr != nil { + w.WriteHeader(httpErr.code) + h.sendMsg(fmt.Sprintf(`No "Authorization" header: %v`, httpErr.message)) + return + } + creds, err := base64.StdEncoding.DecodeString(string(payload)) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + h.sendMsg(fmt.Sprintf(`Badly formed "Authorization" header`)) + return + } + parts := strings.Split(string(creds), ":") + if len(parts) != 2 { + w.WriteHeader(http.StatusBadRequest) + h.sendMsg(fmt.Sprintf(`Badly formed "Authorization" header (2)`)) + return + } + user := parts[0] + password := parts[1] + if user != "bar" || password != "baz" { + w.WriteHeader(http.StatusUnauthorized) + h.sendMsg(fmt.Sprintf("Bad credentials: %q", string(creds))) + return + } + case Oauth: + payload, httpErr := getAuthPayload(r, "Bearer") + if httpErr != nil { + w.WriteHeader(httpErr.code) + h.sendMsg(fmt.Sprintf(`No "Authorization" header: %v`, httpErr.message)) + return + } + if payload != "sometoken" { + w.WriteHeader(http.StatusUnauthorized) + h.sendMsg(fmt.Sprintf(`Bad token: %q`, payload)) + return + } + default: + panic("Woe is me!") + } + h.sendMsg(fmt.Sprintf("Trying to serve %q", r.URL.String())) + switch filepath.Base(r.URL.Path) { + case "prog.aci": + h.sendMsg(fmt.Sprintf(" serving")) + if data, err := h.tools.prepareACI(); err != nil { + w.WriteHeader(http.StatusInternalServerError) + h.sendMsg(fmt.Sprintf(" failed (%v)", err)) + } else { + w.Write(data) + h.sendMsg(fmt.Sprintf(" done.")) + } + default: + h.sendMsg(fmt.Sprintf(" not found.")) + w.WriteHeader(http.StatusNotFound) + } +} + +func (h *serverHandler) sendMsg(msg string) { + select { + case h.msg <- msg: + default: + } +} + +func getAuthPayload(r *http.Request, authType string) (string, *httpError) { + auth := r.Header.Get("Authorization") + if auth == "" { + err := &httpError{ + code: http.StatusUnauthorized, + message: "No auth", + } + return "", err + } + parts := strings.Split(auth, " ") + if len(parts) != 2 { + err := &httpError{ + code: http.StatusBadRequest, + message: "Malformed auth", + } + return "", err + } + if parts[0] != authType { + err := &httpError{ + code: http.StatusUnauthorized, + message: "Wrong auth", + } + return "", err + } + return parts[1], nil +} + +type Server struct { + Stop <-chan struct{} + Msg <-chan string + Conf string + URL string + handler *serverHandler + http *httptest.Server +} + +func (s *Server) Close() { + s.http.Close() + close(s.handler.msg) + close(s.handler.stop) +} + +func NewServer(auth Type, msgCapacity int) (*Server, error) { + return NewServerWithPaths(auth, msgCapacity, "actool", "go") +} + +func NewServerWithPaths(auth Type, msgCapacity int, acTool, goTool string) (*Server, error) { + if !filepath.IsAbs(acTool) { + absAcTool, err := getTool(acTool) + if err != nil { + return nil, err + } + acTool = absAcTool + } + if !filepath.IsAbs(goTool) { + absGoTool, err := getTool(goTool) + if err != nil { + return nil, err + } + goTool = absGoTool + } + stop := make(chan struct{}) + msg := make(chan string, msgCapacity) + server := &Server{ + Stop: stop, + Msg: msg, + handler: &serverHandler{ + auth: auth, + stop: stop, + msg: msg, + tools: &aciToolkit{ + acTool: acTool, + goTool: goTool, + }, + }, + } + server.http = httptest.NewUnstartedServer(server.handler) + server.http.TLS = &tls.Config{InsecureSkipVerify: true} + server.http.StartTLS() + server.URL = server.http.URL + host := server.http.Listener.Addr().String() + switch auth { + case None: + // nothing to do + case Basic: + creds := `"user": "bar", + "password": "baz"` + server.Conf = sprintCreds(host, "basic", creds) + case Oauth: + creds := `"token": "sometoken"` + server.Conf = sprintCreds(host, "oauth", creds) + default: + panic("Woe is me!") + } + return server, nil +} + +func getTool(tool string) (string, error) { + toolPath, err := exec.LookPath(tool) + if err != nil { + return "", fmt.Errorf("failed to find %s in $PATH: $v", tool, err) + } + absToolPath, err := filepath.Abs(toolPath) + if err != nil { + return "", fmt.Errorf("failed to get absolute path of %s: %v", tool, err) + } + return absToolPath, nil +} + +func sprintCreds(host, auth, creds string) string { + return fmt.Sprintf(` +{ + "rktKind": "auth", + "rktVersion": "v1", + "domains": ["%s"], + "type": "%s", + "credentials": + { + %s + } +} + +`, host, auth, creds) +} diff --git a/tests/test-auth-server/main.go b/tests/test-auth-server/main.go new file mode 100644 index 0000000..e9b1abc --- /dev/null +++ b/tests/test-auth-server/main.go @@ -0,0 +1,101 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "os" + + taas "github.com/coreos/rkt/tests/test-auth-server/aci" +) + +func main() { + cmdsStr := "start, stop" + if len(os.Args) < 2 { + fmt.Printf("Error: expected a command - %s\n", cmdsStr) + os.Exit(1) + } + var err error + switch os.Args[1] { + case "start": + err = start(os.Args[2:]) + case "stop": + err = stop(os.Args[2:]) + default: + err = fmt.Errorf("wrong command %q, should be %s", os.Args[1], cmdsStr) + } + if err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } +} + +func start(args []string) error { + typesStr := "none, basic, oauth" + if len(args) < 1 { + return fmt.Errorf("expected a type - %s", typesStr) + } + types := map[string]taas.Type{ + "none": taas.None, + "basic": taas.Basic, + "oauth": taas.Oauth, + } + auth, ok := types[args[0]] + if !ok { + return fmt.Errorf("wrong type %q, should, be %s", args[0], typesStr) + } + server, err := taas.StartServer(auth) + if err != nil { + return fmt.Errorf("failed to start server: %v", err) + } + if server.Conf != "" { + fmt.Printf(server.Conf) + } + fmt.Printf("Ready, waiting for connections at %s\n", server.URL) + loop(server) + fmt.Println("Byebye") + return nil +} + +func loop(server *taas.Server) { + for { + select { + case <-server.Stop: + server.Close() + return + case msg, ok := <-server.Msg: + if ok { + fmt.Println(msg) + } + } + } +} + +func stop(args []string) error { + if len(args) < 1 { + return fmt.Errorf("expected a host") + } + host := args[0] + res, err := taas.StopServer(host) + if err != nil { + return fmt.Errorf("failed to stop server: %v", err) + } + defer res.Body.Close() + fmt.Printf("Response status: %s\n", res.Status) + if res.StatusCode/100 != 2 { + return fmt.Errorf("got a nonsuccess status") + } + return nil +}