<commit_before>// 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"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"github.com/coreos/rkt/Godeps/_workspace/src/github.com/ThomasRooney/gexpect"
	taas "github.com/coreos/rkt/Godeps/_workspace/src/github.com/endocode/test-aci-auth-server/lib"
	"github.com/coreos/rkt/rkt/config"
)

func TestAuthSanity(t *testing.T) {
	skipDestructive(t)
	removeDataDir(t)
	server := runServer(t, taas.None)
	defer server.Close()
	runRktBang(t, server.URL, "none1")
}

func TestAuthBasic(t *testing.T) {
	skipDestructive(t)
	removeDataDir(t)
	defer removeAllConfig(t)
	server := runServer(t, taas.Basic)
	defer server.Close()
	basicNoConfig(t, server)
	basicWithCustomConfig(t, server)
	basicWithVendorConfig(t, server)
}

func basicNoConfig(t *testing.T, server *taas.Server) {
	removeAllConfig(t)
	runRkt401(t, server.URL, "basic-no-config")
}

func basicWithCustomConfig(t *testing.T, server *taas.Server) {
	removeAllConfig(t)
	writeCustomConfig(t, "test.json", server.Conf)
	runRktBang(t, server.URL, "basic-custom-config")
}

func basicWithVendorConfig(t *testing.T, server *taas.Server) {
	removeAllConfig(t)
	writeVendorConfig(t, "test.json", server.Conf)
	runRktBang(t, server.URL, "basic-vendor-config")
}

func TestAuthOauth(t *testing.T) {
	skipDestructive(t)
	removeDataDir(t)
	defer removeAllConfig(t)
	server := runServer(t, taas.Oauth)
	defer server.Close()
	oauthNoConfig(t, server)
	oauthCustomConfig(t, server)
	oauthVendorConfig(t, server)
}

func oauthNoConfig(t *testing.T, server *taas.Server) {
	removeAllConfig(t)
	runRkt401(t, server.URL, "oauth1")
}

func oauthCustomConfig(t *testing.T, server *taas.Server) {
	removeAllConfig(t)
	writeCustomConfig(t, "test.json", server.Conf)
	runRktBang(t, server.URL, "oauth-custom-config")
}

func oauthVendorConfig(t *testing.T, server *taas.Server) {
	removeAllConfig(t)
	writeVendorConfig(t, "test.json", server.Conf)
	runRktBang(t, server.URL, "oauth-vendor-config")
}

func TestAuthOverride(t *testing.T) {
	skipDestructive(t)
	removeDataDir(t)
	defer removeAllConfig(t)
	server := runServer(t, taas.Oauth)
	defer server.Close()
	validVendorInvalidCustom(t, server)
	invalidVendorValidCustom(t, server)
}

func validVendorInvalidCustom(t *testing.T, server *taas.Server) {
	removeAllConfig(t)
	writeVendorConfig(t, "test.json", server.Conf)
	runRktBang(t, server.URL, "oauth-vvic-1")
	writeCustomConfig(t, "test.json", getInvalidOAuthConfig(server.Conf))
	runRkt401(t, server.URL, "oauth-vvic-2")
}

func invalidVendorValidCustom(t *testing.T, server *taas.Server) {
	removeAllConfig(t)
	writeVendorConfig(t, "test.json", getInvalidOAuthConfig(server.Conf))
	runRkt401(t, server.URL, "oauth-ivvc-1")
	writeCustomConfig(t, "test.json", server.Conf)
	runRktBang(t, server.URL, "oauth-ivvc-2")
}

func TestAuthIgnore(t *testing.T) {
	skipDestructive(t)
	removeDataDir(t)
	defer removeAllConfig(t)
	server := runServer(t, taas.Oauth)
	defer server.Close()
	bogusFiles(t, server)
	subdirectories(t, server)
}

func bogusFiles(t *testing.T, server *taas.Server) {
	removeAllConfig(t)
	writeVendorConfig(t, "README", "This is vendor config")
	writeCustomConfig(t, "README", "This is custom config")
	writeVendorConfig(t, "test.notjson", server.Conf)
	writeCustomConfig(t, "test.notjson", server.Conf)
	runRkt401(t, server.URL, "oauth-bogus-files")
}

func subdirectories(t *testing.T, server *taas.Server) {
	removeAllConfig(t)
	customSubdir := filepath.Join(config.DefaultCustomPath, "subdir")
	vendorSubdir := filepath.Join(config.DefaultVendorPath, "subdir")
	writeConfig(t, customSubdir, "test.json", server.Conf)
	writeConfig(t, vendorSubdir, "test.json", server.Conf)
	runRkt401(t, server.URL, "oauth-subdirectories")
}

func runServer(t *testing.T, auth taas.Type) *taas.Server {
	goTool := getAbs(t, "go")
	acTool := getAbs(t, "../bin/actool")
	server, err := taas.NewServerWithPaths(auth, 20, acTool, goTool)
	if err != nil {
		t.Fatalf("Could not start server: %v", err)
	}
	go serverHandler(t, server)
	return server
}

func getAbs(t *testing.T, tool string) string {
	baseTool := filepath.Base(tool)
	pathTool, err := exec.LookPath(tool)
	if err != nil {
		t.Fatalf("Could not find %s, required to run server: %v", baseTool, err)
	}
	absTool, err := filepath.Abs(pathTool)
	if err != nil {
		t.Fatalf("Could not get absolute path of %s, required to run server: %v", baseTool, err)
	}
	return absTool
}

func serverHandler(t *testing.T, server *taas.Server) {
	for {
		select {
		case _, ok := <-server.Stop:
			if ok {
				t.Log("Closing server")
				server.Close()
			}
			return
		case msg, ok := <-server.Msg:
			if ok {
				t.Logf("server: %v", msg)
			}
		}
	}
}

func runRktBang(t *testing.T, host, dir string) {
	child := runRkt(t, host, dir)
	defer child.Wait()
	expectBang(t, child)
}

func runRkt401(t *testing.T, host, dir string) {
	child := runRkt(t, host, dir)
	defer child.Wait()
	expect401(t, child)
}

// TODO (krnowak): Use --dir option when we also add
// --vendor-config-dir and --custom-config-dir options.
func runRkt(t *testing.T, host, dir string) *gexpect.ExpectSubprocess {
	cmd := rktCmd(fmt.Sprintf(`run %s/%s/prog.aci`, host, dir))
	t.Logf("Running rkt: %s", cmd)
	child, err := gexpect.Spawn(cmd)
	if err != nil {
		t.Fatalf("Failed to run rkt: %v", err)
	}
	return child
}

func rktCmd(rest string) string {
	return fmt.Sprintf(`../bin/rkt --debug --insecure-skip-verify %s`, rest)
}

func expectBang(t *testing.T, child *gexpect.ExpectSubprocess) {
	expectLine(t, child, "BANG!")
}

func expect401(t *testing.T, child *gexpect.ExpectSubprocess) {
	expectLine(t, child, "error downloading ACI: bad HTTP status code: 401")
}

func expectLine(t *testing.T, child *gexpect.ExpectSubprocess, line string) {
	if err := child.Expect(line); err != nil {
		t.Fatalf("Didn't receive expected output %q", line)
	}
}

func removeAllConfig(t *testing.T) {
	dirs := []string{
		authDir(config.DefaultCustomPath),
		authDir(config.DefaultVendorPath),
	}
	for _, p := range dirs {
		if err := os.RemoveAll(p); err != nil {
			t.Fatalf("Failed to remove config directory %q: %v", p, err)
		}
		if err := os.MkdirAll(p, 0755); err != nil {
			t.Fatalf("Failed to create config directory %q: %v", p, err)
		}
	}
}

func writeCustomConfig(t *testing.T, filename, contents string) {
	writeConfig(t, config.DefaultCustomPath, filename, contents)
}

func writeVendorConfig(t *testing.T, filename, contents string) {
	writeConfig(t, config.DefaultVendorPath, filename, contents)
}

func writeConfig(t *testing.T, baseDir, filename, contents string) {
	dir := authDir(baseDir)
	if err := os.MkdirAll(dir, 0755); err != nil {
		t.Fatalf("Failed to create config directory %q: %v", dir, err)
	}
	path := filepath.Join(dir, filename)
	os.Remove(path)
	if err := ioutil.WriteFile(path, []byte(contents), 0644); err != nil {
		t.Fatalf("Failed to write file %q: %v", path, err)
	}
}

func authDir(confDir string) string {
	return filepath.Join(confDir, "auth.d")
}

func getInvalidOAuthConfig(conf string) string {
	return strings.Replace(conf, "sometoken", "someobviouslywrongtoken", 1)
}
<commit_msg>functional tests: Rewrite some tests to table style<commit_after>// 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"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"github.com/coreos/rkt/Godeps/_workspace/src/github.com/ThomasRooney/gexpect"
	taas "github.com/coreos/rkt/Godeps/_workspace/src/github.com/endocode/test-aci-auth-server/lib"
	"github.com/coreos/rkt/rkt/config"
)

func TestAuthSanity(t *testing.T) {
	skipDestructive(t)
	removeDataDir(t)
	server := runServer(t, taas.None)
	defer server.Close()
	successfulRunRkt(t, server.URL, "none1")
}

const (
	authSuccessfulDownload = "BANG!"
	authFailedDownload = "error downloading ACI: bad HTTP status code: 401"
)

type genericAuthTest struct {
	name          string
	useServerConf bool
	confDir       string
	line          string
}

func TestAuthBasic(t *testing.T) {
	tests := []genericAuthTest{
		{"basic-no-config", false, "", authFailedDownload},
		{"basic-custom-config", true, config.DefaultCustomPath, authSuccessfulDownload},
		{"basic-vendor-config", true, config.DefaultVendorPath, authSuccessfulDownload},
	}
	testAuthGeneric(t, taas.Basic, tests)
}

func TestAuthOauth(t *testing.T) {
	tests := []genericAuthTest{
		{"oauth-no-config", false, "", authFailedDownload},
		{"oauth-custom-config", true, config.DefaultCustomPath, authSuccessfulDownload},
		{"oauth-vendor-config", true, config.DefaultVendorPath, authSuccessfulDownload},
	}
	testAuthGeneric(t, taas.Oauth, tests)
}

func testAuthGeneric(t *testing.T, auth taas.Type, tests []genericAuthTest) {
	skipDestructive(t)
	removeDataDir(t)
	defer removeAllConfig(t)
	server := runServer(t, auth)
	defer server.Close()
	for _, tt := range tests {
		removeAllConfig(t)
		if tt.useServerConf {
			writeConfig(t, tt.confDir, "test.json", server.Conf)
		}
		expectedRunRkt(t, server.URL, tt.name, tt.line)
	}
}

func TestAuthOverride(t *testing.T) {
	skipDestructive(t)
	removeDataDir(t)
	defer removeAllConfig(t)
	server := runServer(t, taas.Oauth)
	defer server.Close()
	tests := []struct {
		vendorConfig         string
		customConfig         string
		name                 string
		resultBeforeOverride string
		resultAfterOverride  string
	}{
		{server.Conf, getInvalidOAuthConfig(server.Conf), "oauth-vvic", authSuccessfulDownload, authFailedDownload},
		{getInvalidOAuthConfig(server.Conf), server.Conf, "oauth-ivvc", authFailedDownload, authSuccessfulDownload},
	}
	for _, tt := range tests {
		removeAllConfig(t)
		writeVendorConfig(t, "test.json", tt.vendorConfig)
		expectedRunRkt(t, server.URL, tt.name+"-1", tt.resultBeforeOverride)
		writeCustomConfig(t, "test.json", tt.customConfig)
		expectedRunRkt(t, server.URL, tt.name+"-2", tt.resultAfterOverride)
	}
}

func TestAuthIgnore(t *testing.T) {
	skipDestructive(t)
	removeDataDir(t)
	defer removeAllConfig(t)
	server := runServer(t, taas.Oauth)
	defer server.Close()
	testAuthIgnoreBogusFiles(t, server)
	testAuthIgnoreSubdirectories(t, server)
}

func testAuthIgnoreBogusFiles(t *testing.T, server *taas.Server) {
	removeAllConfig(t)
	writeVendorConfig(t, "README", "This is vendor config")
	writeCustomConfig(t, "README", "This is custom config")
	writeVendorConfig(t, "test.notjson", server.Conf)
	writeCustomConfig(t, "test.notjson", server.Conf)
	failedRunRkt(t, server.URL, "oauth-bogus-files")
}

func testAuthIgnoreSubdirectories(t *testing.T, server *taas.Server) {
	removeAllConfig(t)
	customSubdir := filepath.Join(config.DefaultCustomPath, "subdir")
	vendorSubdir := filepath.Join(config.DefaultVendorPath, "subdir")
	writeConfig(t, customSubdir, "test.json", server.Conf)
	writeConfig(t, vendorSubdir, "test.json", server.Conf)
	failedRunRkt(t, server.URL, "oauth-subdirectories")
}

func runServer(t *testing.T, auth taas.Type) *taas.Server {
	goTool := getAbs(t, "go")
	acTool := getAbs(t, "../bin/actool")
	server, err := taas.NewServerWithPaths(auth, 20, acTool, goTool)
	if err != nil {
		t.Fatalf("Could not start server: %v", err)
	}
	go serverHandler(t, server)
	return server
}

func getAbs(t *testing.T, tool string) string {
	baseTool := filepath.Base(tool)
	pathTool, err := exec.LookPath(tool)
	if err != nil {
		t.Fatalf("Could not find %s, required to run server: %v", baseTool, err)
	}
	absTool, err := filepath.Abs(pathTool)
	if err != nil {
		t.Fatalf("Could not get absolute path of %s, required to run server: %v", baseTool, err)
	}
	return absTool
}

func serverHandler(t *testing.T, server *taas.Server) {
	for {
		select {
		case _, ok := <-server.Stop:
			if ok {
				t.Log("Closing server")
				server.Close()
			}
			return
		case msg, ok := <-server.Msg:
			if ok {
				t.Logf("server: %v", msg)
			}
		}
	}
}

func successfulRunRkt(t *testing.T, host, dir string) {
	expectedRunRkt(t, host, dir, authSuccessfulDownload)
}

func failedRunRkt(t *testing.T, host, dir string) {
	expectedRunRkt(t, host, dir, authFailedDownload)
}

func expectedRunRkt(t *testing.T, host, dir, line string) {
	child := runRkt(t, host, dir)
	defer child.Wait()
	if err := child.Expect(line); err != nil {
		t.Fatalf("Didn't receive expected output %q", line)
	}
}

// TODO (krnowak): Use --dir option when we also add
// --vendor-config-dir and --custom-config-dir options. Then we can
// remove destructive tests checks.
func runRkt(t *testing.T, host, dir string) *gexpect.ExpectSubprocess {
	cmd := fmt.Sprintf(`../bin/rkt --debug --insecure-skip-verify run %s/%s/prog.aci`, host, dir)
	t.Logf("Running rkt: %s", cmd)
	child, err := gexpect.Spawn(cmd)
	if err != nil {
		t.Fatalf("Failed to run rkt: %v", err)
	}
	return child
}

func removeAllConfig(t *testing.T) {
	dirs := []string{
		authDir(config.DefaultCustomPath),
		authDir(config.DefaultVendorPath),
	}
	for _, p := range dirs {
		if err := os.RemoveAll(p); err != nil {
			t.Fatalf("Failed to remove config directory %q: %v", p, err)
		}
		if err := os.MkdirAll(p, 0755); err != nil {
			t.Fatalf("Failed to create config directory %q: %v", p, err)
		}
	}
}

func writeCustomConfig(t *testing.T, filename, contents string) {
	writeConfig(t, config.DefaultCustomPath, filename, contents)
}

func writeVendorConfig(t *testing.T, filename, contents string) {
	writeConfig(t, config.DefaultVendorPath, filename, contents)
}

func writeConfig(t *testing.T, baseDir, filename, contents string) {
	dir := authDir(baseDir)
	if err := os.MkdirAll(dir, 0755); err != nil {
		t.Fatalf("Failed to create config directory %q: %v", dir, err)
	}
	path := filepath.Join(dir, filename)
	os.Remove(path)
	if err := ioutil.WriteFile(path, []byte(contents), 0644); err != nil {
		t.Fatalf("Failed to write file %q: %v", path, err)
	}
}

func authDir(confDir string) string {
	return filepath.Join(confDir, "auth.d")
}

func getInvalidOAuthConfig(conf string) string {
	return strings.Replace(conf, "sometoken", "someobviouslywrongtoken", 1)
}
<|endoftext|>


<commit_before>// Copyright 2013 Travis Keep. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or
// at http://opensource.org/licenses/BSD-3-Clause.

package functional

// A Consumer of T consumes the T values from a Stream of T.
type Consumer interface {
  // Consume consumes values from Stream s.
  Consume(s Stream) error
}

// ConsumerFunc is an adapter that allows ordinary functions to be used as
//  Consumers.
type ConsumerFunc func(s Stream) error

func (f ConsumerFunc) Consume(s Stream) error {
  return f(s)
}

// CompositeConsumer returns a Consumer that sends values it consumes to each
// one of consumers. The returned Consumer's Consume method reports an error if
// the Consume method in any of consumers reports an error.
// ptr is a *T where T values being consumed are temporarily held;
// copier knows how to copy the values of type T being consumed
// (can be nil if simple assignment should be used). If caller passes a slice
// for consumers, no copy is made of it.
func CompositeConsumer(
    ptr interface{},
    copier Copier,
    consumers ...Consumer) Consumer {
  switch len(consumers) {
    case 0:
      return nilConsumer{}
    case 1:
      return consumers[0]
    default:
      return ConsumerFunc(func(s Stream) error {
        errors := MultiConsume(s, ptr, copier, consumers...)
        for _, e := range errors {
          if e != nil {
            return e
          }
        }
        return nil
      })
  }
}

// FilterConsumer creates a new Consumer whose Consume method applies f to the
// Stream before passing it onto c.
func FilterConsumer(c Consumer, f Filterer) Consumer {
  return ModifyConsumer(
      c,
      func(s Stream) Stream {
        return Filter(f, s)
      })
}

// ModifyConsumer returns a new Consumer
// that applies f to its Stream and then gives the resulting Stream to c.
// If c is a Consumer of T and f takes a Stream of U and returns a Stream of T,
// then ModifyConsumer returns a Consumer of U.
// The Consume method of the returned Consumer will close the Stream that f
// returns but not the original Stream. It does this by wrapping the
// original Stream with NoCloseStream.
func ModifyConsumer(
    c Consumer,
    f func(s Stream) Stream) Consumer {
  return ConsumerFunc(func(s Stream) (err error) {
    newS := f(NoCloseStream(s))
    defer func() {
      ce := newS.Close()
      if err == nil {
        err = ce
      }
    }()
    err = c.Consume(newS)
    return
  })
}

// MultiConsume consumes the values of s, a Stream of T, sending those T
// values to each Consumer in consumers. MultiConsume consumes values from s
// until no Consumer in consumers is accepting values.
// ptr is a *T that receives the values from s. copier is a Copier
// of T used to copy T values to the Streams sent to each Consumer in
// consumers. Passing null for copier means use simple assignment.
// MultiConsume returns all the errors from the individual Consume methods.
// The order of the returned errors matches the order of the consumers.
func MultiConsume(s Stream, ptr interface{}, copier Copier, consumers ...Consumer) (errors []error) {
  consumerLen := len(consumers)
  if consumerLen == 0 {
    return
  }
  errors = make([]error, consumerLen)
  if consumerLen == 1 {
    errors[0] = consumers[0].Consume(s)
    return
  }

  if copier == nil {
    copier = assignCopier
  }
  streams := make([]splitStream, consumerLen)
  for i := range streams {
    streams[i] = splitStream{&emitterStream{ptrCh: make(chan interface{}), errCh: make(chan error)}}
    go func(s splitStream, c Consumer, e *error) {
      defer s.endStream()
      s.startStream()
      *e = c.Consume(s)
    }(streams[i], consumers[i], &errors[i])
  }
  var err error
  for asyncReturn(streams, err) {
    err = s.Next(ptr)
    for i := range streams {
      if !streams[i].isClosed() {
        p := streams[i].ptr
        copier(ptr, p)
      }
    }
  }
  return
}

// NilConsumer returns a consumer that consumes no values
// NilConsumer is draft API and may change in incompatible ways.
func NilConsumer() Consumer {
  return nilConsumer{}
}

type splitStream struct {
  *emitterStream
}

func (s splitStream) Next(ptr interface{}) error {
  if ptr == nil {
    panic("Got nil pointer in Next.")
  }
  return s.emitterStream.next(ptr)
}

func (s splitStream) Close() error {
  return nil
}

type nilConsumer struct {
}

func (n nilConsumer) Consume(s Stream) error {
  return nil
}

func asyncReturn(streams []splitStream, err error) bool {
  for i := range streams {
    if !streams[i].isClosed() {
      streams[i].errCh <- err
    }
  }
  result := false
  for i := range streams {
    if !streams[i].isClosed() {
      streams[i].ptr = <-streams[i].ptrCh
      if streams[i].ptr == nil {
        streams[i].close()
      } else {
        result = true
      }
    }
  }
  return result
}
<commit_msg>Stabilize draft API.<commit_after>// Copyright 2013 Travis Keep. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or
// at http://opensource.org/licenses/BSD-3-Clause.

package functional

// A Consumer of T consumes the T values from a Stream of T.
type Consumer interface {
  // Consume consumes values from Stream s.
  Consume(s Stream) error
}

// ConsumerFunc is an adapter that allows ordinary functions to be used as
//  Consumers.
type ConsumerFunc func(s Stream) error

func (f ConsumerFunc) Consume(s Stream) error {
  return f(s)
}

// CompositeConsumer returns a Consumer that sends values it consumes to each
// one of consumers. The returned Consumer's Consume method reports an error if
// the Consume method in any of consumers reports an error.
// ptr is a *T where T values being consumed are temporarily held;
// copier knows how to copy the values of type T being consumed
// (can be nil if simple assignment should be used). If caller passes a slice
// for consumers, no copy is made of it.
func CompositeConsumer(
    ptr interface{},
    copier Copier,
    consumers ...Consumer) Consumer {
  switch len(consumers) {
    case 0:
      return nilConsumer{}
    case 1:
      return consumers[0]
    default:
      return ConsumerFunc(func(s Stream) error {
        errors := MultiConsume(s, ptr, copier, consumers...)
        for _, e := range errors {
          if e != nil {
            return e
          }
        }
        return nil
      })
  }
}

// FilterConsumer creates a new Consumer whose Consume method applies f to the
// Stream before passing it onto c.
func FilterConsumer(c Consumer, f Filterer) Consumer {
  return ModifyConsumer(
      c,
      func(s Stream) Stream {
        return Filter(f, s)
      })
}

// ModifyConsumer returns a new Consumer
// that applies f to its Stream and then gives the resulting Stream to c.
// If c is a Consumer of T and f takes a Stream of U and returns a Stream of T,
// then ModifyConsumer returns a Consumer of U.
// The Consume method of the returned Consumer will close the Stream that f
// returns but not the original Stream. It does this by wrapping the
// original Stream with NoCloseStream.
func ModifyConsumer(
    c Consumer,
    f func(s Stream) Stream) Consumer {
  return ConsumerFunc(func(s Stream) (err error) {
    newS := f(NoCloseStream(s))
    defer func() {
      ce := newS.Close()
      if err == nil {
        err = ce
      }
    }()
    err = c.Consume(newS)
    return
  })
}

// MultiConsume consumes the values of s, a Stream of T, sending those T
// values to each Consumer in consumers. MultiConsume consumes values from s
// until no Consumer in consumers is accepting values.
// ptr is a *T that receives the values from s. copier is a Copier
// of T used to copy T values to the Streams sent to each Consumer in
// consumers. Passing null for copier means use simple assignment.
// MultiConsume returns all the errors from the individual Consume methods.
// The order of the returned errors matches the order of the consumers.
func MultiConsume(s Stream, ptr interface{}, copier Copier, consumers ...Consumer) (errors []error) {
  consumerLen := len(consumers)
  if consumerLen == 0 {
    return
  }
  errors = make([]error, consumerLen)
  if consumerLen == 1 {
    errors[0] = consumers[0].Consume(s)
    return
  }

  if copier == nil {
    copier = assignCopier
  }
  streams := make([]splitStream, consumerLen)
  for i := range streams {
    streams[i] = splitStream{&emitterStream{ptrCh: make(chan interface{}), errCh: make(chan error)}}
    go func(s splitStream, c Consumer, e *error) {
      defer s.endStream()
      s.startStream()
      *e = c.Consume(s)
    }(streams[i], consumers[i], &errors[i])
  }
  var err error
  for asyncReturn(streams, err) {
    err = s.Next(ptr)
    for i := range streams {
      if !streams[i].isClosed() {
        p := streams[i].ptr
        copier(ptr, p)
      }
    }
  }
  return
}

// NilConsumer returns a consumer that consumes no values.
func NilConsumer() Consumer {
  return nilConsumer{}
}

type splitStream struct {
  *emitterStream
}

func (s splitStream) Next(ptr interface{}) error {
  if ptr == nil {
    panic("Got nil pointer in Next.")
  }
  return s.emitterStream.next(ptr)
}

func (s splitStream) Close() error {
  return nil
}

type nilConsumer struct {
}

func (n nilConsumer) Consume(s Stream) error {
  return nil
}

func asyncReturn(streams []splitStream, err error) bool {
  for i := range streams {
    if !streams[i].isClosed() {
      streams[i].errCh <- err
    }
  }
  result := false
  for i := range streams {
    if !streams[i].isClosed() {
      streams[i].ptr = <-streams[i].ptrCh
      if streams[i].ptr == nil {
        streams[i].close()
      } else {
        result = true
      }
    }
  }
  return result
}
<|endoftext|>


<commit_before>package functional

import (
	"fmt"
	"html/template"
	"net"
	"net/url"
	"os"
	"strconv"
	"testing"

	"github.com/coreos/dex/connector"
	"gopkg.in/ldap.v2"
)

var (
	ldapHost   string
	ldapPort   uint16
	ldapBindDN string
	ldapBindPw string
)

type LDAPServer struct {
	Host   string
	Port   uint16
	BindDN string
	BindPw string
}

const (
	ldapEnvHost     = "DEX_TEST_LDAP_HOST"
	ldapEnvBindName = "DEX_TEST_LDAP_BINDNAME"
	ldapEnvBindPass = "DEX_TEST_LDAP_BINDPASS"
)

func ldapServer(t *testing.T) LDAPServer {
	host := os.Getenv(ldapEnvHost)
	if host == "" {
		t.Fatalf("%s not set", ldapEnvHost)
	}
	var port uint64 = 389
	if h, p, err := net.SplitHostPort(host); err == nil {
		port, err = strconv.ParseUint(p, 10, 16)
		if err != nil {
			t.Fatalf("failed to parse port: %v", err)
		}
		host = h
	}
	return LDAPServer{host, uint16(port), os.Getenv(ldapEnvBindName), os.Getenv(ldapEnvBindPass)}
}

func TestLDAPConnect(t *testing.T) {
	server := ldapServer(t)
	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", server.Host, server.Port))
	if err != nil {
		t.Fatal(err)
	}
	err = l.Bind(server.BindDN, server.BindPw)
	if err != nil {
		t.Fatal(err)
	}
	l.Close()
}

func TestConnectorLDAPHealthy(t *testing.T) {
	server := ldapServer(t)

	tests := []struct {
		config  connector.LDAPConnectorConfig
		wantErr bool
	}{
		{
			config: connector.LDAPConnectorConfig{
				ID:         "ldap",
				ServerHost: server.Host,
				ServerPort: server.Port + 1,
			},
			wantErr: true,
		},
		{
			config: connector.LDAPConnectorConfig{
				ID:         "ldap",
				ServerHost: server.Host,
				ServerPort: server.Port,
			},
		},
		{
			config: connector.LDAPConnectorConfig{
				ID:         "ldap",
				ServerHost: server.Host,
				ServerPort: server.Port,
				UseTLS:     true,
				CertFile:   "/tmp/ldap.crt",
				KeyFile:    "/tmp/ldap.key",
				CaFile:     "/tmp/openldap-ca.pem",
			},
		},
		{
			config: connector.LDAPConnectorConfig{
				ID:         "ldap",
				ServerHost: server.Host,
				ServerPort: server.Port + 247, // 636
				UseSSL:     true,
				CertFile:   "/tmp/ldap.crt",
				KeyFile:    "/tmp/ldap.key",
				CaFile:     "/tmp/openldap-ca.pem",
			},
		},
	}
	for i, tt := range tests {
		templates := template.New(connector.LDAPLoginPageTemplateName)
		c, err := tt.config.Connector(url.URL{}, nil, templates)
		if err != nil {
			t.Errorf("case %d: failed to create connector: %v", i, err)
			continue
		}
		if err := c.Healthy(); err != nil {
			if !tt.wantErr {
				t.Errorf("case %d: Healthy() returned error: %v", i, err)
			}
		} else if tt.wantErr {
			t.Errorf("case %d: expected Healthy() to fail", i)
		}
	}
}
<commit_msg>Functional tests for LDAP Connection Pool<commit_after>package functional

import (
	"fmt"
	"html/template"
	"net"
	"net/url"
	"os"
	"strconv"
	"sync"
	"testing"
	"time"

	"github.com/coreos/dex/connector"
	"golang.org/x/net/context"
	"gopkg.in/ldap.v2"
)

var (
	ldapHost   string
	ldapPort   uint16
	ldapBindDN string
	ldapBindPw string
)

type LDAPServer struct {
	Host   string
	Port   uint16
	BindDN string
	BindPw string
}

const (
	ldapEnvHost     = "DEX_TEST_LDAP_HOST"
	ldapEnvBindName = "DEX_TEST_LDAP_BINDNAME"
	ldapEnvBindPass = "DEX_TEST_LDAP_BINDPASS"
)

func ldapServer(t *testing.T) LDAPServer {
	host := os.Getenv(ldapEnvHost)
	if host == "" {
		t.Fatalf("%s not set", ldapEnvHost)
	}
	var port uint64 = 389
	if h, p, err := net.SplitHostPort(host); err == nil {
		port, err = strconv.ParseUint(p, 10, 16)
		if err != nil {
			t.Fatalf("failed to parse port: %v", err)
		}
		host = h
	}
	return LDAPServer{host, uint16(port), os.Getenv(ldapEnvBindName), os.Getenv(ldapEnvBindPass)}
}

func TestLDAPConnect(t *testing.T) {
	server := ldapServer(t)
	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", server.Host, server.Port))
	if err != nil {
		t.Fatal(err)
	}
	err = l.Bind(server.BindDN, server.BindPw)
	if err != nil {
		t.Fatal(err)
	}
	l.Close()
}

func TestConnectorLDAPHealthy(t *testing.T) {
	server := ldapServer(t)

	tests := []struct {
		config  connector.LDAPConnectorConfig
		wantErr bool
	}{
		{
			config: connector.LDAPConnectorConfig{
				ID:         "ldap",
				ServerHost: server.Host,
				ServerPort: server.Port + 1,
			},
			wantErr: true,
		},
		{
			config: connector.LDAPConnectorConfig{
				ID:         "ldap",
				ServerHost: server.Host,
				ServerPort: server.Port,
			},
		},
		{
			config: connector.LDAPConnectorConfig{
				ID:         "ldap",
				ServerHost: server.Host,
				ServerPort: server.Port,
				UseTLS:     true,
				CertFile:   "/tmp/ldap.crt",
				KeyFile:    "/tmp/ldap.key",
				CaFile:     "/tmp/openldap-ca.pem",
			},
		},
		{
			config: connector.LDAPConnectorConfig{
				ID:         "ldap",
				ServerHost: server.Host,
				ServerPort: server.Port + 247, // 636
				UseSSL:     true,
				CertFile:   "/tmp/ldap.crt",
				KeyFile:    "/tmp/ldap.key",
				CaFile:     "/tmp/openldap-ca.pem",
			},
		},
	}
	for i, tt := range tests {
		templates := template.New(connector.LDAPLoginPageTemplateName)
		c, err := tt.config.Connector(url.URL{}, nil, templates)
		if err != nil {
			t.Errorf("case %d: failed to create connector: %v", i, err)
			continue
		}
		if err := c.Healthy(); err != nil {
			if !tt.wantErr {
				t.Errorf("case %d: Healthy() returned error: %v", i, err)
			}
		} else if tt.wantErr {
			t.Errorf("case %d: expected Healthy() to fail", i)
		}
	}
}

func TestLDAPPoolHighWatermarkAndLockContention(t *testing.T) {
	server := ldapServer(t)
	ldapPool := &connector.LDAPPool{
		MaxIdleConn: 30,
		ServerHost:  server.Host,
		ServerPort:  server.Port,
		UseTLS:      false,
		UseSSL:      false,
	}

	// Excercise pool operations with MaxIdleConn + 10 concurrent goroutines.
	// We are testing both pool high watermark code and lock contention
	numRoutines := ldapPool.MaxIdleConn + 10
	var wg sync.WaitGroup
	wg.Add(numRoutines)
	ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
	for i := 0; i < numRoutines; i++ {
		go func() {
			defer wg.Done()
			for {
				select {
				case <-ctx.Done():
					return
				default:
					ldapConn, err := ldapPool.Acquire()
					if err != nil {
						t.Errorf("Unable to acquire LDAP Connection: %v", err)
					}
					s := ldap.NewSearchRequest("", ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, "(objectClass=*)", []string{}, nil)
					_, err = ldapConn.Search(s)
					if err != nil {
						t.Errorf("Search request failed. Dead/invalid LDAP connection from pool?: %v", err)
						ldapConn.Close()
					} else {
						ldapPool.Put(ldapConn)
					}
					_, _ = ldapPool.CheckConnections()
				}
			}
		}()
	}

	// Wait for all operations to complete and check status.
	// There should be MaxIdleConn connections in the pool. This confirms:
	// 1. The tests was indeed executed concurrently
	// 2. Even though we ran more routines than the configured MaxIdleConn the high
	//    watermark code did its job and closed surplus connections
	wg.Wait()
	alive, killed := ldapPool.CheckConnections()
	if alive < ldapPool.MaxIdleConn {
		t.Errorf("expected %v connections, got alive=%v killed=%v", ldapPool.MaxIdleConn, alive, killed)
	}
}
<|endoftext|>


<commit_before>package imaging

import (
	"bytes"
	"io"
	"math"
	"os"
	"time"

	// this is the only outside pixur package dependency.  Avoid depending too much on schema.
	"pixur.org/pixur/be/status"

	// unfortuantely, most of my machines are stuck on 6.8.x.x
	// TODO: update this to v2 when possible.
	"gopkg.in/gographics/imagick.v1/imagick"
)

const (
	DefaultGifFormat  ImageFormat = "GIF"
	DefaultJpegFormat ImageFormat = "JPEG"
	DefaultPngFormat  ImageFormat = "PNG"
)

const gifTicksPerSecond = 100

func init() {
	imagick.Initialize()
	// never call Terminate
}

// a list of all formats: mw.QueryFormats("*")

// ImageFormat is the string format of the image
type ImageFormat string

func (f ImageFormat) IsGif() bool {
	return f == "GIF" || f == "GIF87"
}

func (f ImageFormat) IsJpeg() bool {
	return f == "JPE" || f == "JPEG" || f == "JPG"
}

func (f ImageFormat) IsPng() bool {
	// These are the types imagick says it supports: PNG PNG00 PNG24 PNG32 PNG48 PNG64 PNG8.  I am
	// only including the ones I know.
	return f == "PNG" || f == "PNG24" || f == "PNG32" || f == "PNG8"
}

type PixurImage2 interface {
	Format() ImageFormat
	Dimensions() (width, height int64, sts status.S)
	// In the future, this could also include a histogram
	Duration() (*time.Duration, status.S)

	Thumbnail(width, height int64, format ImageFormat) (PixurImage2, status.S)

	Write(io.Writer) status.S

	Close()
}

var _ PixurImage2 = (*pixurImage2)(nil)

type pixurImage2 struct {
	mw *imagick.MagickWand
}

func (pi *pixurImage2) Write(io.Writer) status.S {
  return nil
}

func (pi *pixurImage2) Thumbnail(width, height int64, format ImageFormat) (PixurImage2, status.S) {
  newmw := pi.mw.Clone()
  // TODO: crop, set colorspace, trim profiles, optimize
  newpi := &pixurImage2{
    mw: newmw,
  }
  return newpi, nil
}

func (pi *pixurImage2) Format() ImageFormat {
	return ImageFormat(pi.mw.GetImageFormat())
}

func (pi *pixurImage2) Dimensions() (int64, int64, status.S) {
	w, h, _, _, err := pi.mw.GetImagePage()
	if err != nil {
		return 0, 0, status.InternalError(err, "unable to get image dimensions")
	}
	return int64(w), int64(h), nil
}

func (pi *pixurImage2) Duration() (*time.Duration, status.S) {
	if !pi.Format().IsGif() {
		return nil, nil
	}
	if tps := pi.mw.GetImageTicksPerSecond(); tps != gifTicksPerSecond {
		return nil, status.InternalErrorf(nil, "Wrong ticks per second %v", tps)
	}
	switch pi.mw.GetNumberImages() {
	case 1:
		return nil, nil
	case 0:
		return nil, status.InvalidArgument(nil, "no images")
	}
	var d time.Duration
	pi.mw.ResetIterator()
	const tickDuration = time.Second / gifTicksPerSecond
	const delayTicksMax = math.MaxInt64 / int64(tickDuration)
	for {
		delayTicks := int64(pi.mw.GetImageDelay())
		if delayTicks < 0 || delayTicks > delayTicksMax {
			return nil, status.InvalidArgument(nil, "delayTicks would overflow", delayTicks)
		}
		// this should always be positive
		delayTickDuration := time.Duration(delayTicks) * tickDuration
		if d += delayTickDuration; d < 0 {
			return nil, status.InvalidArgument(nil, "duration overflow", d)
		}

		if !pi.mw.NextImage() {
			break
		}
	}

	return &d, nil
}

func (pi *pixurImage2) Close() {
	if pi.mw != nil {
		pi.mw.Destroy()
		pi.mw = nil
	}
}

func ReadImage2(r io.Reader) (PixurImage2, status.S) {
	mw := imagick.NewMagickWand()
	destroy := true
	defer func() {
		if destroy {
			mw.Destroy()
		}
	}()
	switch r := r.(type) {
	case *os.File:
		if err := mw.ReadImageFile(r); err != nil {
			return nil, status.InvalidArgument(err, "unable to decode image")
		}
	default:
		var b bytes.Buffer
		if _, err := io.Copy(&b, r); err != nil {
			return nil, status.InvalidArgument(err, "unable to copy image")
		}
		if err := mw.ReadImageBlob(b.Bytes()); err != nil {
			return nil, status.InvalidArgument(err, "unable to decode image")
		}
	}
	pi := &pixurImage2{mw: mw}
	destroy = false
	return pi, nil
}
<commit_msg>be/imaging: add thumbnail support<commit_after>package imaging

import (
	"bytes"
	"io"
	"math"
	"os"
	"time"

	// this is the only outside pixur package dependency.  Avoid depending too much on schema.
	"pixur.org/pixur/be/status"

	// unfortuantely, most of my machines are stuck on 6.8.x.x
	// TODO: update this to v2 when possible.
	"gopkg.in/gographics/imagick.v1/imagick"
)

const (
	// WEBM magic header
	ebmlHeader = "\x1a\x45\xdf\xa3"
)

const (
	DefaultGifFormat  ImageFormat = "GIF"
	DefaultJpegFormat ImageFormat = "JPEG"
	DefaultPngFormat  ImageFormat = "PNG"
)

const (
	thumbnailSquareSize = 160
)

const gifTicksPerSecond = 100

func init() {
	imagick.Initialize()
	// never call Terminate
}

// a list of all formats: mw.QueryFormats("*")

// ImageFormat is the string format of the image
type ImageFormat string

// IsGif returns true if the type of this image is a GIF.
func (f ImageFormat) IsGif() bool {
	return f == "GIF" || f == "GIF87"
}

// IsJpeg returns true if the type of this image is a JPEG.
func (f ImageFormat) IsJpeg() bool {
	return f == "JPE" || f == "JPEG" || f == "JPG"
}

// IsPng returns true if the type of this image is a PNG.
func (f ImageFormat) IsPng() bool {
	// These are the types imagick says it supports: PNG PNG00 PNG24 PNG32 PNG48 PNG64 PNG8.  I am
	// only including the ones I know.
	return f == "PNG" || f == "PNG24" || f == "PNG32" || f == "PNG8"
}

type PixurImage2 interface {
	Format() ImageFormat
	Dimensions() (width, height uint, sts status.S)
	// In the future, this could also include a histogram
	Duration() (*time.Duration, status.S)

	Thumbnail() (PixurImage2, status.S)

	Write(io.Writer) status.S

	Close()
}

var _ PixurImage2 = (*pixurImage2)(nil)

type pixurImage2 struct {
	mw *imagick.MagickWand
}

func (pi *pixurImage2) Write(w io.Writer) status.S {

	switch w := w.(type) {
	case *os.File:
		if err := pi.mw.WriteImageFile(w); err != nil {
			return status.InternalError(err, "can't write image file")
		}
	default:
		panic("uh oh")
	}

	return nil
}

func (pi *pixurImage2) Thumbnail() (PixurImage2, status.S) {
	w, h, sts := pi.Dimensions()
	if sts != nil {
		return nil, sts
	}
	var neww, newh uint
	var x, y int
	if w > h {
		neww = h
		newh = h
		x = int((w - neww) / 2)
		y = 0
	} else {
		neww = w
		newh = h
		x = 0
		y = int((h - newh) / 2)
	}
	newmw := pi.mw.Clone()
	newmw.ResetIterator()
	destroy := true
	defer func() {
		if destroy {
			newmw.Destroy()
		}
	}()

	if err := newmw.CropImage(newh, neww, x, y); err != nil {
		return nil, status.InternalError(err, "unable to crop thumbnail")
	}

	side := uint(thumbnailSquareSize)
	newmw.TransformImageColorspace(imagick.COLORSPACE_LAB)
	if err := newmw.ResizeImage(side, side, imagick.FILTER_LANCZOS2_SHARP, 1); err != nil {
		return nil, status.InternalError(err, "unable to resize thumbnail")
	}
	newmw.TransformImageColorspace(imagick.COLORSPACE_SRGB)

	for _, p := range newmw.GetImageProfiles("*") {
		switch p {
		case "icc":
			fallthrough // usually big
		default:
			newmw.RemoveImageProfile(p)
		}
	}

	for _, p := range newmw.GetImageProperties("*") {
		// I found these by looking through all properties of images I had and seeing which ones
		// ImageMagick reads (rather than informitively sets)
		switch p {
		case "png:bit-depth-written":
		case "png:IHDR.color-type-orig":
		case "png:IHDR.bit-depth-orig":

		case "jpeg:colorspace":
			fallthrough // we only want srgb
		case "jpeg:sampling-factor":
			fallthrough // we set this later
		case "date:modify":
			fallthrough // don't care
		case "png:tIME":
			fallthrough // don't care
		default:
			if err := newmw.DeleteImageProperty(p); err != nil {
				return nil, status.InternalError(err, "unable to delete property ", p)
			}
		}
	}

	format := pi.Format()
	switch {
	case format.IsGif():
	case format.IsPng():

	default:
		if err := newmw.SetImageFormat(string(DefaultJpegFormat)); err != nil {
			return nil, status.InternalError(err, "unable to set format")
		}
		fallthrough
	case format.IsJpeg():
		newmw.SetImageCompressionQuality(90)
		if err := newmw.SetOption("jpeg:sampling-factor", "1x1,1x1,1x1"); err != nil {
			return nil, status.InternalError(err, "unable to set sampling factor")
		}
	}

	// TODO:trim profiles (keep colorspace?), apply orientation,
	newpi := &pixurImage2{
		mw: newmw,
	}
	destroy = false
	return newpi, nil
}

func (pi *pixurImage2) Format() ImageFormat {
	return ImageFormat(pi.mw.GetImageFormat())
}

func (pi *pixurImage2) Dimensions() (uint, uint, status.S) {
	w, h, _, _, err := pi.mw.GetImagePage()
	if err != nil {
		return 0, 0, status.InternalError(err, "unable to get image dimensions")
	}
	return w, h, nil
}

func (pi *pixurImage2) Duration() (*time.Duration, status.S) {
	if !pi.Format().IsGif() {
		return nil, nil
	}
	if tps := pi.mw.GetImageTicksPerSecond(); tps != gifTicksPerSecond {
		return nil, status.InternalErrorf(nil, "Wrong ticks per second %v", tps)
	}
	switch pi.mw.GetNumberImages() {
	case 1:
		return nil, nil
	case 0:
		return nil, status.InvalidArgument(nil, "no images")
	}
	var d time.Duration
	pi.mw.ResetIterator()
	const tickDuration = time.Second / gifTicksPerSecond
	const delayTicksMax = math.MaxInt64 / int64(tickDuration)
	for {
		delayTicks := int64(pi.mw.GetImageDelay())
		if delayTicks < 0 || delayTicks > delayTicksMax {
			return nil, status.InvalidArgument(nil, "delayTicks would overflow", delayTicks)
		}
		// this should always be positive
		delayTickDuration := time.Duration(delayTicks) * tickDuration
		if d += delayTickDuration; d < 0 {
			return nil, status.InvalidArgument(nil, "duration overflow", d)
		}

		if !pi.mw.NextImage() {
			break
		}
	}

	return &d, nil
}

func (pi *pixurImage2) Close() {
	if pi.mw != nil {
		pi.mw.Destroy()
		pi.mw = nil
	}
}

func ReadImage2(r io.Reader) (PixurImage2, status.S) {
	mw := imagick.NewMagickWand()
	destroy := true
	defer func() {
		if destroy {
			mw.Destroy()
		}
	}()
	switch r := r.(type) {
	case *os.File:
		if err := mw.ReadImageFile(r); err != nil {
			return nil, status.InvalidArgument(err, "unable to decode image")
		}
	default:
		var b bytes.Buffer
		if _, err := io.Copy(&b, r); err != nil {
			return nil, status.InvalidArgument(err, "unable to copy image")
		}
		if err := mw.ReadImageBlob(b.Bytes()); err != nil {
			return nil, status.InvalidArgument(err, "unable to decode image")
		}
	}
	pi := &pixurImage2{mw: mw}
	destroy = false
	return pi, nil
}
<|endoftext|>


<commit_before>package worker

import (
	"archive/zip"
	"bytes"
	"encoding/json"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"time"

	"github.com/iron-io/iron_go/api"
)

type Schedule struct {
	CodeName       string         `json:"code_name"`
	Delay          *time.Duration `json:"delay"`
	EndAt          *time.Time     `json:"end_at"`
	MaxConcurrency *int           `json:"max_concurrency"`
	Name           string         `json:"name"`
	Payload        string         `json:"payload"`
	Priority       *int           `json:"priority"`
	RunEvery       *int           `json:"run_every"`
	RunTimes       *int           `json:"run_times"`
	StartAt        *time.Time     `json:"start_at"`
	Cluster        string         `json:"cluster"`
	Label          string         `json:"label"`
}

type ScheduleInfo struct {
	CodeName       string    `json:"code_name"`
	CreatedAt      time.Time `json:"created_at"`
	EndAt          time.Time `json:"end_at"`
	Id             string    `json:"id"`
	LastRunTime    time.Time `json:"last_run_time"`
	MaxConcurrency int       `json:"max_concurrency"`
	Msg            string    `json:"msg"`
	NextStart      time.Time `json:"next_start"`
	ProjectId      string    `json:"project_id"`
	RunCount       int       `json:"run_count"`
	RunTimes       int       `json:"run_times"`
	StartAt        time.Time `json:"start_at"`
	Status         string    `json:"status"`
	UpdatedAt      time.Time `json:"updated_at"`
}

type Task struct {
	CodeName string         `json:"code_name"`
	Payload  string         `json:"payload"`
	Priority int            `json:"priority"`
	Timeout  *time.Duration `json:"timeout"`
	Delay    *time.Duration `json:"delay"`
	Cluster  string         `json:"cluster"`
	Label    string         `json:"label"`
}

type TaskInfo struct {
	CodeHistoryId string    `json:"code_history_id"`
	CodeId        string    `json:"code_id"`
	CodeName      string    `json:"code_name"`
	CodeRev       string    `json:"code_rev"`
	Id            string    `json:"id"`
	Payload       string    `json:"payload"`
	ProjectId     string    `json:"project_id"`
	Status        string    `json:"status"`
	Msg           string    `json:"msg,omitempty"`
	ScheduleId    string    `json:"schedule_id"`
	Duration      int       `json:"duration"`
	RunTimes      int       `json:"run_times"`
	Timeout       int       `json:"timeout"`
	Percent       int       `json:"percent,omitempty"`
	CreatedAt     time.Time `json:"created_at"`
	UpdatedAt     time.Time `json:"updated_at"`
	StartTime     time.Time `json:"start_time"`
	EndTime       time.Time `json:"end_time"`
}

type CodeSource map[string][]byte // map[pathInZip]code

type Code struct {
	Name           string        `json:"name"`
	Runtime        string        `json:"runtime"`
	FileName       string        `json:"file_name"`
	Config         string        `json:"config,omitempty"`
	MaxConcurrency int           `json:"max_concurrency,omitempty"`
	Retries        int           `json:"retries,omitempty"`
	Stack          string        `json:"stack"`
	Image          string        `json:"image"`
	Command        string        `json:"command"`
	RetriesDelay   time.Duration `json:"-"`
	Source         CodeSource    `json:"-"`
}

type CodeInfo struct {
	Id              string    `json:"id"`
	LatestChecksum  string    `json:"latest_checksum"`
	LatestHistoryId string    `json:"latest_history_id"`
	Name            string    `json:"name"`
	ProjectId       string    `json:"project_id"`
	Runtime         *string   `json:"runtime"`
	Rev             int       `json:"rev"`
	CreatedAt       time.Time `json:"created_at"`
	UpdatedAt       time.Time `json:"updated_at"`
	LatestChange    time.Time `json:"latest_change"`
}

// CodePackageList lists code packages.
//
// The page argument decides the page of code packages you want to retrieve, starting from 0, maximum is 100.
//
// The perPage argument determines the number of code packages to return. Note
// this is a maximum value, so there may be fewer packages returned if there
// aren’t enough results. If this is < 1, 1 will be the default. Maximum is 100.
func (w *Worker) CodePackageList(page, perPage int) (codes []CodeInfo, err error) {
	out := map[string][]CodeInfo{}

	err = w.codes().
		QueryAdd("page", "%d", page).
		QueryAdd("per_page", "%d", perPage).
		Req("GET", nil, &out)
	if err != nil {
		return
	}

	return out["codes"], nil
}

// CodePackageUpload uploads a code package
func (w *Worker) CodePackageUpload(code Code) (id string, err error) {
	client := http.Client{}

	body := &bytes.Buffer{}
	mWriter := multipart.NewWriter(body)

	// write meta-data
	mMetaWriter, err := mWriter.CreateFormField("data")
	if err != nil {
		return
	}
	jEncoder := json.NewEncoder(mMetaWriter)
	err = jEncoder.Encode(map[string]interface{}{
		"name":            code.Name,
		"runtime":         code.Runtime,
		"file_name":       code.FileName,
		"config":          code.Config,
		"max_concurrency": code.MaxConcurrency,
		"retries":         code.Retries,
		"retries_delay":   code.RetriesDelay.Seconds(),
	})
	if err != nil {
		return
	}

	// write the zip
	mFileWriter, err := mWriter.CreateFormFile("file", "worker.zip")
	if err != nil {
		return
	}
	zWriter := zip.NewWriter(mFileWriter)

	for sourcePath, sourceText := range code.Source {
		fWriter, err := zWriter.Create(sourcePath)
		if err != nil {
			return "", err
		}
		fWriter.Write([]byte(sourceText))
	}

	zWriter.Close()

	// done with multipart
	mWriter.Close()

	req, err := http.NewRequest("POST", w.codes().URL.String(), body)
	if err != nil {
		return
	}

	req.Header.Set("Accept", "application/json")
	req.Header.Set("Accept-Encoding", "gzip/deflate")
	req.Header.Set("Authorization", "OAuth "+w.Settings.Token)
	req.Header.Set("Content-Type", mWriter.FormDataContentType())
	req.Header.Set("User-Agent", w.Settings.UserAgent)

	// dumpRequest(req) NOTE: never do this here, it breaks stuff
	response, err := client.Do(req)
	if err != nil {
		return
	}
	if err = api.ResponseAsError(response); err != nil {
		return
	}

	// dumpResponse(response)

	data := struct {
		Id         string `json:"id"`
		Msg        string `json:"msg"`
		StatusCode int    `json:"status_code"`
	}{}
	err = json.NewDecoder(response.Body).Decode(&data)
	if err != nil {
		return
	}

	return data.Id, err
}

// CodePackageInfo gets info about a code package
func (w *Worker) CodePackageInfo(codeId string) (code CodeInfo, err error) {
	out := CodeInfo{}
	err = w.codes(codeId).Req("GET", nil, &out)
	return out, err
}

// CodePackageDelete deletes a code package
func (w *Worker) CodePackageDelete(codeId string) (err error) {
	return w.codes(codeId).Req("DELETE", nil, nil)
}

// CodePackageDownload downloads a code package
func (w *Worker) CodePackageDownload(codeId string) (code Code, err error) {
	out := Code{}
	err = w.codes(codeId, "download").Req("GET", nil, &out)
	return out, err
}

// CodePackageRevisions lists the revisions of a code pacakge
func (w *Worker) CodePackageRevisions(codeId string) (code Code, err error) {
	out := Code{}
	err = w.codes(codeId, "revisions").Req("GET", nil, &out)
	return out, err
}

func (w *Worker) TaskList() (tasks []TaskInfo, err error) {
	out := map[string][]TaskInfo{}
	err = w.tasks().Req("GET", nil, &out)
	if err != nil {
		return
	}
	return out["tasks"], nil
}

type TaskListParams struct {
	CodeName string
	Page     int
	PerPage  int
	FromTime time.Time
	ToTime   time.Time
	Statuses []string
}

func (w *Worker) FilteredTaskList(params TaskListParams) (tasks []TaskInfo, err error) {
	out := map[string][]TaskInfo{}
	url := w.tasks()

	url.QueryAdd("code_name", "%s", params.CodeName)

	if params.Page > 0 {
		url.QueryAdd("page", "%d", params.Page)
	}

	if params.PerPage > 0 {
		url.QueryAdd("per_page", "%d", params.PerPage)
	}

	if fromTimeSeconds := params.FromTime.Unix(); fromTimeSeconds > 0 {
		url.QueryAdd("from_time", "%d", fromTimeSeconds)
	}

	if toTimeSeconds := params.ToTime.Unix(); toTimeSeconds > 0 {
		url.QueryAdd("to_time", "%d", toTimeSeconds)
	}

	for _, status := range params.Statuses {
		url.QueryAdd(status, "%d", true)
	}

	err = url.Req("GET", nil, &out)

	if err != nil {
		return
	}

	return out["tasks"], nil
}

// TaskQueue queues a task
func (w *Worker) TaskQueue(tasks ...Task) (taskIds []string, err error) {
	outTasks := make([]map[string]interface{}, 0, len(tasks))

	for _, task := range tasks {
		thisTask := map[string]interface{}{
			"code_name": task.CodeName,
			"payload":   task.Payload,
			"priority":  task.Priority,
			"cluster":   task.Cluster,
			"label":     task.Label,
		}
		if task.Timeout != nil {
			thisTask["timeout"] = (*task.Timeout).Seconds()
		}
		if task.Delay != nil {
			thisTask["delay"] = int64((*task.Delay).Seconds())
		}

		outTasks = append(outTasks, thisTask)
	}

	in := map[string][]map[string]interface{}{"tasks": outTasks}
	out := struct {
		Tasks []struct {
			Id string `json:"id"`
		} `json:"tasks"`
		Msg string `json:"msg"`
	}{}

	err = w.tasks().Req("POST", &in, &out)
	if err != nil {
		return
	}

	taskIds = make([]string, 0, len(out.Tasks))
	for _, task := range out.Tasks {
		taskIds = append(taskIds, task.Id)
	}

	return
}

// TaskInfo gives info about a given task
func (w *Worker) TaskInfo(taskId string) (task TaskInfo, err error) {
	out := TaskInfo{}
	err = w.tasks(taskId).Req("GET", nil, &out)
	return out, err
}

func (w *Worker) TaskLog(taskId string) (log []byte, err error) {
	response, err := w.tasks(taskId, "log").Request("GET", nil)
	if err != nil {
		return
	}

	log, err = ioutil.ReadAll(response.Body)
	return
}

// TaskCancel cancels a Task
func (w *Worker) TaskCancel(taskId string) (err error) {
	_, err = w.tasks(taskId, "cancel").Request("POST", nil)
	return err
}

// TaskProgress sets a Task's Progress
func (w *Worker) TaskProgress(taskId string, progress int, msg string) (err error) {
	payload := map[string]interface{}{
		"msg":     msg,
		"percent": progress,
	}

	err = w.tasks(taskId, "progress").Req("POST", payload, nil)
	return
}

// TaskQueueWebhook queues a Task from a Webhook
func (w *Worker) TaskQueueWebhook() (err error) { return }

// ScheduleList lists Scheduled Tasks
func (w *Worker) ScheduleList() (schedules []ScheduleInfo, err error) {
	out := map[string][]ScheduleInfo{}
	err = w.schedules().Req("GET", nil, &out)
	if err != nil {
		return
	}
	return out["schedules"], nil
}

// Schedule a Task
func (w *Worker) Schedule(schedules ...Schedule) (scheduleIds []string, err error) {
	outSchedules := make([]map[string]interface{}, 0, len(schedules))

	for _, schedule := range schedules {
		sm := map[string]interface{}{
			"code_name": schedule.CodeName,
			"name":      schedule.Name,
			"payload":   schedule.Payload,
			"label":     schedule.Label,
			"cluster":   schedule.Cluster,
		}
		if schedule.Delay != nil {
			sm["delay"] = (*schedule.Delay).Seconds()
		}
		if schedule.EndAt != nil {
			sm["end_at"] = *schedule.EndAt
		}
		if schedule.MaxConcurrency != nil {
			sm["max_concurrency"] = *schedule.MaxConcurrency
		}
		if schedule.Priority != nil {
			sm["priority"] = *schedule.Priority
		}
		if schedule.RunEvery != nil {
			sm["run_every"] = *schedule.RunEvery
		}
		if schedule.RunTimes != nil {
			sm["run_times"] = *schedule.RunTimes
		}
		if schedule.StartAt != nil {
			sm["start_at"] = *schedule.StartAt
		}
		outSchedules = append(outSchedules, sm)
	}

	in := map[string][]map[string]interface{}{"schedules": outSchedules}
	out := struct {
		Schedules []struct {
			Id string `json:"id"`
		} `json:"schedules"`
		Msg string `json:"msg"`
	}{}

	err = w.schedules().Req("POST", &in, &out)
	if err != nil {
		return
	}

	scheduleIds = make([]string, 0, len(out.Schedules))

	for _, schedule := range out.Schedules {
		scheduleIds = append(scheduleIds, schedule.Id)
	}

	return
}

// ScheduleInfo gets info about a scheduled task
func (w *Worker) ScheduleInfo(scheduleId string) (info ScheduleInfo, err error) {
	info = ScheduleInfo{}
	err = w.schedules(scheduleId).Req("GET", nil, &info)
	return info, nil
}

// ScheduleCancel cancels a scheduled task
func (w *Worker) ScheduleCancel(scheduleId string) (err error) {
	_, err = w.schedules(scheduleId, "cancel").Request("POST", nil)
	return
}
<commit_msg>Added a couple fields to Code.<commit_after>package worker

import (
	"archive/zip"
	"bytes"
	"encoding/json"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"time"

	"github.com/iron-io/iron_go/api"
)

type Schedule struct {
	CodeName       string         `json:"code_name"`
	Delay          *time.Duration `json:"delay"`
	EndAt          *time.Time     `json:"end_at"`
	MaxConcurrency *int           `json:"max_concurrency"`
	Name           string         `json:"name"`
	Payload        string         `json:"payload"`
	Priority       *int           `json:"priority"`
	RunEvery       *int           `json:"run_every"`
	RunTimes       *int           `json:"run_times"`
	StartAt        *time.Time     `json:"start_at"`
	Cluster        string         `json:"cluster"`
	Label          string         `json:"label"`
}

type ScheduleInfo struct {
	CodeName       string    `json:"code_name"`
	CreatedAt      time.Time `json:"created_at"`
	EndAt          time.Time `json:"end_at"`
	Id             string    `json:"id"`
	LastRunTime    time.Time `json:"last_run_time"`
	MaxConcurrency int       `json:"max_concurrency"`
	Msg            string    `json:"msg"`
	NextStart      time.Time `json:"next_start"`
	ProjectId      string    `json:"project_id"`
	RunCount       int       `json:"run_count"`
	RunTimes       int       `json:"run_times"`
	StartAt        time.Time `json:"start_at"`
	Status         string    `json:"status"`
	UpdatedAt      time.Time `json:"updated_at"`
}

type Task struct {
	CodeName string         `json:"code_name"`
	Payload  string         `json:"payload"`
	Priority int            `json:"priority"`
	Timeout  *time.Duration `json:"timeout"`
	Delay    *time.Duration `json:"delay"`
	Cluster  string         `json:"cluster"`
	Label    string         `json:"label"`
}

type TaskInfo struct {
	CodeHistoryId string    `json:"code_history_id"`
	CodeId        string    `json:"code_id"`
	CodeName      string    `json:"code_name"`
	CodeRev       string    `json:"code_rev"`
	Id            string    `json:"id"`
	Payload       string    `json:"payload"`
	ProjectId     string    `json:"project_id"`
	Status        string    `json:"status"`
	Msg           string    `json:"msg,omitempty"`
	ScheduleId    string    `json:"schedule_id"`
	Duration      int       `json:"duration"`
	RunTimes      int       `json:"run_times"`
	Timeout       int       `json:"timeout"`
	Percent       int       `json:"percent,omitempty"`
	CreatedAt     time.Time `json:"created_at"`
	UpdatedAt     time.Time `json:"updated_at"`
	StartTime     time.Time `json:"start_time"`
	EndTime       time.Time `json:"end_time"`
}

type CodeSource map[string][]byte // map[pathInZip]code

type Code struct {
	Id             string        `json:"id,omitempty"`
	Name           string        `json:"name"`
	Runtime        string        `json:"runtime"`
	FileName       string        `json:"file_name"`
	Config         string        `json:"config,omitempty"`
	MaxConcurrency int           `json:"max_concurrency,omitempty"`
	Retries        int           `json:"retries,omitempty"`
	Stack          string        `json:"stack"`
	Image          string        `json:"image"`
	Command        string        `json:"command"`
	RetriesDelay   time.Duration `json:"-"`
	Source         CodeSource    `json:"-"`
	Host           string        `json:"host,omitempty"`
}

type CodeInfo struct {
	Id              string    `json:"id"`
	LatestChecksum  string    `json:"latest_checksum"`
	LatestHistoryId string    `json:"latest_history_id"`
	Name            string    `json:"name"`
	ProjectId       string    `json:"project_id"`
	Runtime         *string   `json:"runtime"`
	Rev             int       `json:"rev"`
	CreatedAt       time.Time `json:"created_at"`
	UpdatedAt       time.Time `json:"updated_at"`
	LatestChange    time.Time `json:"latest_change"`
}

// CodePackageList lists code packages.
//
// The page argument decides the page of code packages you want to retrieve, starting from 0, maximum is 100.
//
// The perPage argument determines the number of code packages to return. Note
// this is a maximum value, so there may be fewer packages returned if there
// aren’t enough results. If this is < 1, 1 will be the default. Maximum is 100.
func (w *Worker) CodePackageList(page, perPage int) (codes []CodeInfo, err error) {
	out := map[string][]CodeInfo{}

	err = w.codes().
		QueryAdd("page", "%d", page).
		QueryAdd("per_page", "%d", perPage).
		Req("GET", nil, &out)
	if err != nil {
		return
	}

	return out["codes"], nil
}

// CodePackageUpload uploads a code package
func (w *Worker) CodePackageUpload(code Code) (id string, err error) {
	client := http.Client{}

	body := &bytes.Buffer{}
	mWriter := multipart.NewWriter(body)

	// write meta-data
	mMetaWriter, err := mWriter.CreateFormField("data")
	if err != nil {
		return
	}
	jEncoder := json.NewEncoder(mMetaWriter)
	err = jEncoder.Encode(map[string]interface{}{
		"name":            code.Name,
		"runtime":         code.Runtime,
		"file_name":       code.FileName,
		"config":          code.Config,
		"max_concurrency": code.MaxConcurrency,
		"retries":         code.Retries,
		"retries_delay":   code.RetriesDelay.Seconds(),
	})
	if err != nil {
		return
	}

	// write the zip
	mFileWriter, err := mWriter.CreateFormFile("file", "worker.zip")
	if err != nil {
		return
	}
	zWriter := zip.NewWriter(mFileWriter)

	for sourcePath, sourceText := range code.Source {
		fWriter, err := zWriter.Create(sourcePath)
		if err != nil {
			return "", err
		}
		fWriter.Write([]byte(sourceText))
	}

	zWriter.Close()

	// done with multipart
	mWriter.Close()

	req, err := http.NewRequest("POST", w.codes().URL.String(), body)
	if err != nil {
		return
	}

	req.Header.Set("Accept", "application/json")
	req.Header.Set("Accept-Encoding", "gzip/deflate")
	req.Header.Set("Authorization", "OAuth "+w.Settings.Token)
	req.Header.Set("Content-Type", mWriter.FormDataContentType())
	req.Header.Set("User-Agent", w.Settings.UserAgent)

	// dumpRequest(req) NOTE: never do this here, it breaks stuff
	response, err := client.Do(req)
	if err != nil {
		return
	}
	if err = api.ResponseAsError(response); err != nil {
		return
	}

	// dumpResponse(response)

	data := struct {
		Id         string `json:"id"`
		Msg        string `json:"msg"`
		StatusCode int    `json:"status_code"`
	}{}
	err = json.NewDecoder(response.Body).Decode(&data)
	if err != nil {
		return
	}

	return data.Id, err
}

// CodePackageInfo gets info about a code package
func (w *Worker) CodePackageInfo(codeId string) (code CodeInfo, err error) {
	out := CodeInfo{}
	err = w.codes(codeId).Req("GET", nil, &out)
	return out, err
}

// CodePackageDelete deletes a code package
func (w *Worker) CodePackageDelete(codeId string) (err error) {
	return w.codes(codeId).Req("DELETE", nil, nil)
}

// CodePackageDownload downloads a code package
func (w *Worker) CodePackageDownload(codeId string) (code Code, err error) {
	out := Code{}
	err = w.codes(codeId, "download").Req("GET", nil, &out)
	return out, err
}

// CodePackageRevisions lists the revisions of a code pacakge
func (w *Worker) CodePackageRevisions(codeId string) (code Code, err error) {
	out := Code{}
	err = w.codes(codeId, "revisions").Req("GET", nil, &out)
	return out, err
}

func (w *Worker) TaskList() (tasks []TaskInfo, err error) {
	out := map[string][]TaskInfo{}
	err = w.tasks().Req("GET", nil, &out)
	if err != nil {
		return
	}
	return out["tasks"], nil
}

type TaskListParams struct {
	CodeName string
	Page     int
	PerPage  int
	FromTime time.Time
	ToTime   time.Time
	Statuses []string
}

func (w *Worker) FilteredTaskList(params TaskListParams) (tasks []TaskInfo, err error) {
	out := map[string][]TaskInfo{}
	url := w.tasks()

	url.QueryAdd("code_name", "%s", params.CodeName)

	if params.Page > 0 {
		url.QueryAdd("page", "%d", params.Page)
	}

	if params.PerPage > 0 {
		url.QueryAdd("per_page", "%d", params.PerPage)
	}

	if fromTimeSeconds := params.FromTime.Unix(); fromTimeSeconds > 0 {
		url.QueryAdd("from_time", "%d", fromTimeSeconds)
	}

	if toTimeSeconds := params.ToTime.Unix(); toTimeSeconds > 0 {
		url.QueryAdd("to_time", "%d", toTimeSeconds)
	}

	for _, status := range params.Statuses {
		url.QueryAdd(status, "%d", true)
	}

	err = url.Req("GET", nil, &out)

	if err != nil {
		return
	}

	return out["tasks"], nil
}

// TaskQueue queues a task
func (w *Worker) TaskQueue(tasks ...Task) (taskIds []string, err error) {
	outTasks := make([]map[string]interface{}, 0, len(tasks))

	for _, task := range tasks {
		thisTask := map[string]interface{}{
			"code_name": task.CodeName,
			"payload":   task.Payload,
			"priority":  task.Priority,
			"cluster":   task.Cluster,
			"label":     task.Label,
		}
		if task.Timeout != nil {
			thisTask["timeout"] = (*task.Timeout).Seconds()
		}
		if task.Delay != nil {
			thisTask["delay"] = int64((*task.Delay).Seconds())
		}

		outTasks = append(outTasks, thisTask)
	}

	in := map[string][]map[string]interface{}{"tasks": outTasks}
	out := struct {
		Tasks []struct {
			Id string `json:"id"`
		} `json:"tasks"`
		Msg string `json:"msg"`
	}{}

	err = w.tasks().Req("POST", &in, &out)
	if err != nil {
		return
	}

	taskIds = make([]string, 0, len(out.Tasks))
	for _, task := range out.Tasks {
		taskIds = append(taskIds, task.Id)
	}

	return
}

// TaskInfo gives info about a given task
func (w *Worker) TaskInfo(taskId string) (task TaskInfo, err error) {
	out := TaskInfo{}
	err = w.tasks(taskId).Req("GET", nil, &out)
	return out, err
}

func (w *Worker) TaskLog(taskId string) (log []byte, err error) {
	response, err := w.tasks(taskId, "log").Request("GET", nil)
	if err != nil {
		return
	}

	log, err = ioutil.ReadAll(response.Body)
	return
}

// TaskCancel cancels a Task
func (w *Worker) TaskCancel(taskId string) (err error) {
	_, err = w.tasks(taskId, "cancel").Request("POST", nil)
	return err
}

// TaskProgress sets a Task's Progress
func (w *Worker) TaskProgress(taskId string, progress int, msg string) (err error) {
	payload := map[string]interface{}{
		"msg":     msg,
		"percent": progress,
	}

	err = w.tasks(taskId, "progress").Req("POST", payload, nil)
	return
}

// TaskQueueWebhook queues a Task from a Webhook
func (w *Worker) TaskQueueWebhook() (err error) { return }

// ScheduleList lists Scheduled Tasks
func (w *Worker) ScheduleList() (schedules []ScheduleInfo, err error) {
	out := map[string][]ScheduleInfo{}
	err = w.schedules().Req("GET", nil, &out)
	if err != nil {
		return
	}
	return out["schedules"], nil
}

// Schedule a Task
func (w *Worker) Schedule(schedules ...Schedule) (scheduleIds []string, err error) {
	outSchedules := make([]map[string]interface{}, 0, len(schedules))

	for _, schedule := range schedules {
		sm := map[string]interface{}{
			"code_name": schedule.CodeName,
			"name":      schedule.Name,
			"payload":   schedule.Payload,
			"label":     schedule.Label,
			"cluster":   schedule.Cluster,
		}
		if schedule.Delay != nil {
			sm["delay"] = (*schedule.Delay).Seconds()
		}
		if schedule.EndAt != nil {
			sm["end_at"] = *schedule.EndAt
		}
		if schedule.MaxConcurrency != nil {
			sm["max_concurrency"] = *schedule.MaxConcurrency
		}
		if schedule.Priority != nil {
			sm["priority"] = *schedule.Priority
		}
		if schedule.RunEvery != nil {
			sm["run_every"] = *schedule.RunEvery
		}
		if schedule.RunTimes != nil {
			sm["run_times"] = *schedule.RunTimes
		}
		if schedule.StartAt != nil {
			sm["start_at"] = *schedule.StartAt
		}
		outSchedules = append(outSchedules, sm)
	}

	in := map[string][]map[string]interface{}{"schedules": outSchedules}
	out := struct {
		Schedules []struct {
			Id string `json:"id"`
		} `json:"schedules"`
		Msg string `json:"msg"`
	}{}

	err = w.schedules().Req("POST", &in, &out)
	if err != nil {
		return
	}

	scheduleIds = make([]string, 0, len(out.Schedules))

	for _, schedule := range out.Schedules {
		scheduleIds = append(scheduleIds, schedule.Id)
	}

	return
}

// ScheduleInfo gets info about a scheduled task
func (w *Worker) ScheduleInfo(scheduleId string) (info ScheduleInfo, err error) {
	info = ScheduleInfo{}
	err = w.schedules(scheduleId).Req("GET", nil, &info)
	return info, nil
}

// ScheduleCancel cancels a scheduled task
func (w *Worker) ScheduleCancel(scheduleId string) (err error) {
	_, err = w.schedules(scheduleId, "cancel").Request("POST", nil)
	return
}
<|endoftext|>


<commit_before>package worker

import (
	"archive/zip"
	"bytes"
	"encoding/json"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"time"

	"github.com/iron-io/iron_go/api"
)

type Schedule struct {
	CodeName       string         `json:"code_name"`
	Delay          *time.Duration `json:"delay"`
	EndAt          *time.Time     `json:"end_at"`
	MaxConcurrency *int           `json:"max_concurrency"`
	Name           string         `json:"name"`
	Payload        string         `json:"payload"`
	Priority       *int           `json:"priority"`
	RunEvery       *int           `json:"run_every"`
	RunTimes       *int           `json:"run_times"`
	StartAt        *time.Time     `json:"start_at"`
	Cluster        string         `json:"cluster"`
	Label          string         `json:"label"`
}

type ScheduleInfo struct {
	CodeName       string    `json:"code_name"`
	CreatedAt      time.Time `json:"created_at"`
	EndAt          time.Time `json:"end_at"`
	Id             string    `json:"id"`
	LastRunTime    time.Time `json:"last_run_time"`
	MaxConcurrency int       `json:"max_concurrency"`
	Msg            string    `json:"msg"`
	NextStart      time.Time `json:"next_start"`
	ProjectId      string    `json:"project_id"`
	RunCount       int       `json:"run_count"`
	RunTimes       int       `json:"run_times"`
	StartAt        time.Time `json:"start_at"`
	Status         string    `json:"status"`
	UpdatedAt      time.Time `json:"updated_at"`
}

type Task struct {
	CodeName string         `json:"code_name"`
	Payload  string         `json:"payload"`
	Priority int            `json:"priority"`
	Timeout  *time.Duration `json:"timeout"`
	Delay    *time.Duration `json:"delay"`
	Cluster  string         `json:"cluster"`
	Label    string         `json:"label"`
}

type TaskInfo struct {
	CodeHistoryId string    `json:"code_history_id"`
	CodeId        string    `json:"code_id"`
	CodeName      string    `json:"code_name"`
	CodeRev       string    `json:"code_rev"`
	Id            string    `json:"id"`
	Payload       string    `json:"payload"`
	ProjectId     string    `json:"project_id"`
	Status        string    `json:"status"`
	Msg           string    `json:"msg,omitempty"`
	Duration      int       `json:"duration"`
	RunTimes      int       `json:"run_times"`
	Timeout       int       `json:"timeout"`
	Percent       int       `json:"percent,omitempty"`
	CreatedAt     time.Time `json:"created_at"`
	UpdatedAt     time.Time `json:"updated_at"`
	StartTime     time.Time `json:"start_time"`
	EndTime       time.Time `json:"end_time"`
}

type CodeSource map[string][]byte // map[pathInZip]code

type Code struct {
	Name           string        `json:"name"`
	Runtime        string        `json:"runtime"`
	FileName       string        `json:"file_name"`
	Config         string        `json:"config,omitempty"`
	MaxConcurrency int           `json:"max_concurrency,omitempty"`
	Retries        int           `json:"retries,omitempty"`
	Stack          string        `json:"stack"`
	RetriesDelay   time.Duration `json:"-"`
	Source         CodeSource    `json:"-"`
}

type CodeInfo struct {
	Id              string    `json:"id"`
	LatestChecksum  string    `json:"latest_checksum"`
	LatestHistoryId string    `json:"latest_history_id"`
	Name            string    `json:"name"`
	ProjectId       string    `json:"project_id"`
	Runtime         string    `json:"runtime"`
	Rev             int       `json:"rev"`
	CreatedAt       time.Time `json:"created_at"`
	UpdatedAt       time.Time `json:"updated_at"`
	LatestChange    time.Time `json:"latest_change"`
}

// CodePackageList lists code packages.
//
// The page argument decides the page of code packages you want to retrieve, starting from 0, maximum is 100.
//
// The perPage argument determines the number of code packages to return. Note
// this is a maximum value, so there may be fewer packages returned if there
// aren’t enough results. If this is < 1, 1 will be the default. Maximum is 100.
func (w *Worker) CodePackageList(page, perPage int) (codes []CodeInfo, err error) {
	out := map[string][]CodeInfo{}

	err = w.codes().
		QueryAdd("page", "%d", page).
		QueryAdd("per_page", "%d", perPage).
		Req("GET", nil, &out)
	if err != nil {
		return
	}

	return out["codes"], nil
}

// CodePackageUpload uploads a code package
func (w *Worker) CodePackageUpload(code Code) (id string, err error) {
	client := http.Client{}

	body := &bytes.Buffer{}
	mWriter := multipart.NewWriter(body)

	// write meta-data
	mMetaWriter, err := mWriter.CreateFormField("data")
	if err != nil {
		return
	}
	jEncoder := json.NewEncoder(mMetaWriter)
	err = jEncoder.Encode(map[string]interface{}{
		"name":            code.Name,
		"runtime":         code.Runtime,
		"file_name":       code.FileName,
		"config":          code.Config,
		"max_concurrency": code.MaxConcurrency,
		"retries":         code.Retries,
		"retries_delay":   code.RetriesDelay.Seconds(),
	})
	if err != nil {
		return
	}

	// write the zip
	mFileWriter, err := mWriter.CreateFormFile("file", "worker.zip")
	if err != nil {
		return
	}
	zWriter := zip.NewWriter(mFileWriter)

	for sourcePath, sourceText := range code.Source {
		fWriter, err := zWriter.Create(sourcePath)
		if err != nil {
			return "", err
		}
		fWriter.Write([]byte(sourceText))
	}

	zWriter.Close()

	// done with multipart
	mWriter.Close()

	req, err := http.NewRequest("POST", w.codes().URL.String(), body)
	if err != nil {
		return
	}

	req.Header.Set("Accept", "application/json")
	req.Header.Set("Accept-Encoding", "gzip/deflate")
	req.Header.Set("Authorization", "OAuth "+w.Settings.Token)
	req.Header.Set("Content-Type", mWriter.FormDataContentType())
	req.Header.Set("User-Agent", w.Settings.UserAgent)

	// dumpRequest(req) NOTE: never do this here, it breaks stuff
	response, err := client.Do(req)
	if err != nil {
		return
	}
	if err = api.ResponseAsError(response); err != nil {
		return
	}

	// dumpResponse(response)

	data := struct {
		Id         string `json:"id"`
		Msg        string `json:"msg"`
		StatusCode int    `json:"status_code"`
	}{}
	err = json.NewDecoder(response.Body).Decode(&data)
	if err != nil {
		return
	}

	return data.Id, err
}

// CodePackageInfo gets info about a code package
func (w *Worker) CodePackageInfo(codeId string) (code CodeInfo, err error) {
	out := CodeInfo{}
	err = w.codes(codeId).Req("GET", nil, &out)
	return out, err
}

// CodePackageDelete deletes a code package
func (w *Worker) CodePackageDelete(codeId string) (err error) {
	return w.codes(codeId).Req("DELETE", nil, nil)
}

// CodePackageDownload downloads a code package
func (w *Worker) CodePackageDownload(codeId string) (code Code, err error) {
	out := Code{}
	err = w.codes(codeId, "download").Req("GET", nil, &out)
	return out, err
}

// CodePackageRevisions lists the revisions of a code pacakge
func (w *Worker) CodePackageRevisions(codeId string) (code Code, err error) {
	out := Code{}
	err = w.codes(codeId, "revisions").Req("GET", nil, &out)
	return out, err
}

func (w *Worker) TaskList() (tasks []TaskInfo, err error) {
	out := map[string][]TaskInfo{}
	err = w.tasks().Req("GET", nil, &out)
	if err != nil {
		return
	}
	return out["tasks"], nil
}

// TaskQueue queues a task
func (w *Worker) TaskQueue(tasks ...Task) (taskIds []string, err error) {
	outTasks := make([]map[string]interface{}, 0, len(tasks))

	for _, task := range tasks {
		thisTask := map[string]interface{}{
			"code_name": task.CodeName,
			"payload":   task.Payload,
			"priority":  task.Priority,
			"cluster":   task.Cluster,
			"label":     task.Label,
		}
		if task.Timeout != nil {
			thisTask["timeout"] = (*task.Timeout).Seconds()
		}
		if task.Delay != nil {
			thisTask["delay"] = int64((*task.Delay).Seconds())
		}

		outTasks = append(outTasks, thisTask)
	}

	in := map[string][]map[string]interface{}{"tasks": outTasks}
	out := struct {
		Tasks []struct {
			Id string `json:"id"`
		} `json:"tasks"`
		Msg string `json:"msg"`
	}{}

	err = w.tasks().Req("POST", &in, &out)
	if err != nil {
		return
	}

	taskIds = make([]string, 0, len(out.Tasks))
	for _, task := range out.Tasks {
		taskIds = append(taskIds, task.Id)
	}

	return
}

// TaskInfo gives info about a given task
func (w *Worker) TaskInfo(taskId string) (task TaskInfo, err error) {
	out := TaskInfo{}
	err = w.tasks(taskId).Req("GET", nil, &out)
	return out, err
}

func (w *Worker) TaskLog(taskId string) (log []byte, err error) {
	response, err := w.tasks(taskId, "log").Request("GET", nil)
	if err != nil {
		return
	}

	log, err = ioutil.ReadAll(response.Body)
	return
}

// TaskCancel cancels a Task
func (w *Worker) TaskCancel(taskId string) (err error) {
	_, err = w.tasks(taskId, "cancel").Request("POST", nil)
	return err
}

// TaskProgress sets a Task's Progress
func (w *Worker) TaskProgress(taskId string, progress int, msg string) (err error) {
	payload := map[string]interface{}{
		"msg":     msg,
		"percent": progress,
	}

	err = w.tasks(taskId, "progress").Req("POST", payload, nil)
	return
}

// TaskQueueWebhook queues a Task from a Webhook
func (w *Worker) TaskQueueWebhook() (err error) { return }

// ScheduleList lists Scheduled Tasks
func (w *Worker) ScheduleList() (schedules []ScheduleInfo, err error) {
	out := map[string][]ScheduleInfo{}
	err = w.schedules().Req("GET", nil, &out)
	if err != nil {
		return
	}
	return out["schedules"], nil
}

// Schedule a Task
func (w *Worker) Schedule(schedules ...Schedule) (scheduleIds []string, err error) {
	outSchedules := make([]map[string]interface{}, 0, len(schedules))

	for _, schedule := range schedules {
		sm := map[string]interface{}{
			"code_name": schedule.CodeName,
			"name":      schedule.Name,
			"payload":   schedule.Payload,
			"label":     schedule.Label,
			"cluster":   schedule.Cluster,
		}
		if schedule.Delay != nil {
			sm["delay"] = (*schedule.Delay).Seconds()
		}
		if schedule.EndAt != nil {
			sm["end_at"] = *schedule.EndAt
		}
		if schedule.MaxConcurrency != nil {
			sm["max_concurrency"] = *schedule.MaxConcurrency
		}
		if schedule.Priority != nil {
			sm["priority"] = *schedule.Priority
		}
		if schedule.RunEvery != nil {
			sm["run_every"] = *schedule.RunEvery
		}
		if schedule.RunTimes != nil {
			sm["run_times"] = *schedule.RunTimes
		}
		if schedule.StartAt != nil {
			sm["start_at"] = *schedule.StartAt
		}
		outSchedules = append(outSchedules, sm)
	}

	in := map[string][]map[string]interface{}{"schedules": outSchedules}
	out := struct {
		Schedules []struct {
			Id string `json:"id"`
		} `json:"schedules"`
		Msg string `json:"msg"`
	}{}

	err = w.schedules().Req("POST", &in, &out)
	if err != nil {
		return
	}

	scheduleIds = make([]string, 0, len(out.Schedules))

	for _, schedule := range out.Schedules {
		scheduleIds = append(scheduleIds, schedule.Id)
	}

	return
}

// ScheduleInfo gets info about a scheduled task
func (w *Worker) ScheduleInfo(scheduleId string) (info ScheduleInfo, err error) {
	info = ScheduleInfo{}
	err = w.schedules(scheduleId).Req("GET", nil, &info)
	return info, nil
}

// ScheduleCancel cancels a scheduled task
func (w *Worker) ScheduleCancel(scheduleId string) (err error) {
	_, err = w.schedules(scheduleId, "cancel").Request("POST", nil)
	return
}
<commit_msg>Add a method to filter task list<commit_after>package worker

import (
	"archive/zip"
	"bytes"
	"encoding/json"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"time"

	"github.com/iron-io/iron_go/api"
)

type Schedule struct {
	CodeName       string         `json:"code_name"`
	Delay          *time.Duration `json:"delay"`
	EndAt          *time.Time     `json:"end_at"`
	MaxConcurrency *int           `json:"max_concurrency"`
	Name           string         `json:"name"`
	Payload        string         `json:"payload"`
	Priority       *int           `json:"priority"`
	RunEvery       *int           `json:"run_every"`
	RunTimes       *int           `json:"run_times"`
	StartAt        *time.Time     `json:"start_at"`
	Cluster        string         `json:"cluster"`
	Label          string         `json:"label"`
}

type ScheduleInfo struct {
	CodeName       string    `json:"code_name"`
	CreatedAt      time.Time `json:"created_at"`
	EndAt          time.Time `json:"end_at"`
	Id             string    `json:"id"`
	LastRunTime    time.Time `json:"last_run_time"`
	MaxConcurrency int       `json:"max_concurrency"`
	Msg            string    `json:"msg"`
	NextStart      time.Time `json:"next_start"`
	ProjectId      string    `json:"project_id"`
	RunCount       int       `json:"run_count"`
	RunTimes       int       `json:"run_times"`
	StartAt        time.Time `json:"start_at"`
	Status         string    `json:"status"`
	UpdatedAt      time.Time `json:"updated_at"`
}

type Task struct {
	CodeName string         `json:"code_name"`
	Payload  string         `json:"payload"`
	Priority int            `json:"priority"`
	Timeout  *time.Duration `json:"timeout"`
	Delay    *time.Duration `json:"delay"`
	Cluster  string         `json:"cluster"`
	Label    string         `json:"label"`
}

type TaskInfo struct {
	CodeHistoryId string    `json:"code_history_id"`
	CodeId        string    `json:"code_id"`
	CodeName      string    `json:"code_name"`
	CodeRev       string    `json:"code_rev"`
	Id            string    `json:"id"`
	Payload       string    `json:"payload"`
	ProjectId     string    `json:"project_id"`
	Status        string    `json:"status"`
	Msg           string    `json:"msg,omitempty"`
	Duration      int       `json:"duration"`
	RunTimes      int       `json:"run_times"`
	Timeout       int       `json:"timeout"`
	Percent       int       `json:"percent,omitempty"`
	CreatedAt     time.Time `json:"created_at"`
	UpdatedAt     time.Time `json:"updated_at"`
	StartTime     time.Time `json:"start_time"`
	EndTime       time.Time `json:"end_time"`
}

type CodeSource map[string][]byte // map[pathInZip]code

type Code struct {
	Name           string        `json:"name"`
	Runtime        string        `json:"runtime"`
	FileName       string        `json:"file_name"`
	Config         string        `json:"config,omitempty"`
	MaxConcurrency int           `json:"max_concurrency,omitempty"`
	Retries        int           `json:"retries,omitempty"`
	Stack          string        `json:"stack"`
	RetriesDelay   time.Duration `json:"-"`
	Source         CodeSource    `json:"-"`
}

type CodeInfo struct {
	Id              string    `json:"id"`
	LatestChecksum  string    `json:"latest_checksum"`
	LatestHistoryId string    `json:"latest_history_id"`
	Name            string    `json:"name"`
	ProjectId       string    `json:"project_id"`
	Runtime         string    `json:"runtime"`
	Rev             int       `json:"rev"`
	CreatedAt       time.Time `json:"created_at"`
	UpdatedAt       time.Time `json:"updated_at"`
	LatestChange    time.Time `json:"latest_change"`
}

// CodePackageList lists code packages.
//
// The page argument decides the page of code packages you want to retrieve, starting from 0, maximum is 100.
//
// The perPage argument determines the number of code packages to return. Note
// this is a maximum value, so there may be fewer packages returned if there
// aren’t enough results. If this is < 1, 1 will be the default. Maximum is 100.
func (w *Worker) CodePackageList(page, perPage int) (codes []CodeInfo, err error) {
	out := map[string][]CodeInfo{}

	err = w.codes().
		QueryAdd("page", "%d", page).
		QueryAdd("per_page", "%d", perPage).
		Req("GET", nil, &out)
	if err != nil {
		return
	}

	return out["codes"], nil
}

// CodePackageUpload uploads a code package
func (w *Worker) CodePackageUpload(code Code) (id string, err error) {
	client := http.Client{}

	body := &bytes.Buffer{}
	mWriter := multipart.NewWriter(body)

	// write meta-data
	mMetaWriter, err := mWriter.CreateFormField("data")
	if err != nil {
		return
	}
	jEncoder := json.NewEncoder(mMetaWriter)
	err = jEncoder.Encode(map[string]interface{}{
		"name":            code.Name,
		"runtime":         code.Runtime,
		"file_name":       code.FileName,
		"config":          code.Config,
		"max_concurrency": code.MaxConcurrency,
		"retries":         code.Retries,
		"retries_delay":   code.RetriesDelay.Seconds(),
	})
	if err != nil {
		return
	}

	// write the zip
	mFileWriter, err := mWriter.CreateFormFile("file", "worker.zip")
	if err != nil {
		return
	}
	zWriter := zip.NewWriter(mFileWriter)

	for sourcePath, sourceText := range code.Source {
		fWriter, err := zWriter.Create(sourcePath)
		if err != nil {
			return "", err
		}
		fWriter.Write([]byte(sourceText))
	}

	zWriter.Close()

	// done with multipart
	mWriter.Close()

	req, err := http.NewRequest("POST", w.codes().URL.String(), body)
	if err != nil {
		return
	}

	req.Header.Set("Accept", "application/json")
	req.Header.Set("Accept-Encoding", "gzip/deflate")
	req.Header.Set("Authorization", "OAuth "+w.Settings.Token)
	req.Header.Set("Content-Type", mWriter.FormDataContentType())
	req.Header.Set("User-Agent", w.Settings.UserAgent)

	// dumpRequest(req) NOTE: never do this here, it breaks stuff
	response, err := client.Do(req)
	if err != nil {
		return
	}
	if err = api.ResponseAsError(response); err != nil {
		return
	}

	// dumpResponse(response)

	data := struct {
		Id         string `json:"id"`
		Msg        string `json:"msg"`
		StatusCode int    `json:"status_code"`
	}{}
	err = json.NewDecoder(response.Body).Decode(&data)
	if err != nil {
		return
	}

	return data.Id, err
}

// CodePackageInfo gets info about a code package
func (w *Worker) CodePackageInfo(codeId string) (code CodeInfo, err error) {
	out := CodeInfo{}
	err = w.codes(codeId).Req("GET", nil, &out)
	return out, err
}

// CodePackageDelete deletes a code package
func (w *Worker) CodePackageDelete(codeId string) (err error) {
	return w.codes(codeId).Req("DELETE", nil, nil)
}

// CodePackageDownload downloads a code package
func (w *Worker) CodePackageDownload(codeId string) (code Code, err error) {
	out := Code{}
	err = w.codes(codeId, "download").Req("GET", nil, &out)
	return out, err
}

// CodePackageRevisions lists the revisions of a code pacakge
func (w *Worker) CodePackageRevisions(codeId string) (code Code, err error) {
	out := Code{}
	err = w.codes(codeId, "revisions").Req("GET", nil, &out)
	return out, err
}

func (w *Worker) TaskList() (tasks []TaskInfo, err error) {
	out := map[string][]TaskInfo{}
	err = w.tasks().Req("GET", nil, &out)
	if err != nil {
		return
	}
	return out["tasks"], nil
}

type TaskListParams struct {
	CodeName string
	Page     int
	PerPage  int
	FromTime time.Time
	ToTime   time.Time
	Statuses map[string]bool
}

func (w *Worker) FilteredTaskList(params TaskListParams) (tasks []TaskInfo, err error) {
	out := map[string][]TaskInfo{}
	url := w.tasks()

	if params.CodeName != "" {
		url.QueryAdd("code_name", "%s", params.CodeName)
	}

	if params.Page > 0 {
		url.QueryAdd("page", "%d", params.Page)
	}

	if params.PerPage > 0 {
		url.QueryAdd("per_page", "%d", params.PerPage)
	}

	if fromTimeSeconds := params.FromTime.Unix(); fromTimeSeconds > 0 {
		url.QueryAdd("from_time", "%d", fromTimeSeconds)
	}

	if toTimeSeconds := params.ToTime.Unix(); toTimeSeconds > 0 {
		url.QueryAdd("to_time", "%d", toTimeSeconds)
	}

	for status, value := range params.Statuses {
		url.QueryAdd(status, "%d", value)
	}

	err = url.Req("GET", nil, &out)

	if err != nil {
		return
	}

	return out["tasks"], nil
}

// TaskQueue queues a task
func (w *Worker) TaskQueue(tasks ...Task) (taskIds []string, err error) {
	outTasks := make([]map[string]interface{}, 0, len(tasks))

	for _, task := range tasks {
		thisTask := map[string]interface{}{
			"code_name": task.CodeName,
			"payload":   task.Payload,
			"priority":  task.Priority,
			"cluster":   task.Cluster,
			"label":     task.Label,
		}
		if task.Timeout != nil {
			thisTask["timeout"] = (*task.Timeout).Seconds()
		}
		if task.Delay != nil {
			thisTask["delay"] = int64((*task.Delay).Seconds())
		}

		outTasks = append(outTasks, thisTask)
	}

	in := map[string][]map[string]interface{}{"tasks": outTasks}
	out := struct {
		Tasks []struct {
			Id string `json:"id"`
		} `json:"tasks"`
		Msg string `json:"msg"`
	}{}

	err = w.tasks().Req("POST", &in, &out)
	if err != nil {
		return
	}

	taskIds = make([]string, 0, len(out.Tasks))
	for _, task := range out.Tasks {
		taskIds = append(taskIds, task.Id)
	}

	return
}

// TaskInfo gives info about a given task
func (w *Worker) TaskInfo(taskId string) (task TaskInfo, err error) {
	out := TaskInfo{}
	err = w.tasks(taskId).Req("GET", nil, &out)
	return out, err
}

func (w *Worker) TaskLog(taskId string) (log []byte, err error) {
	response, err := w.tasks(taskId, "log").Request("GET", nil)
	if err != nil {
		return
	}

	log, err = ioutil.ReadAll(response.Body)
	return
}

// TaskCancel cancels a Task
func (w *Worker) TaskCancel(taskId string) (err error) {
	_, err = w.tasks(taskId, "cancel").Request("POST", nil)
	return err
}

// TaskProgress sets a Task's Progress
func (w *Worker) TaskProgress(taskId string, progress int, msg string) (err error) {
	payload := map[string]interface{}{
		"msg":     msg,
		"percent": progress,
	}

	err = w.tasks(taskId, "progress").Req("POST", payload, nil)
	return
}

// TaskQueueWebhook queues a Task from a Webhook
func (w *Worker) TaskQueueWebhook() (err error) { return }

// ScheduleList lists Scheduled Tasks
func (w *Worker) ScheduleList() (schedules []ScheduleInfo, err error) {
	out := map[string][]ScheduleInfo{}
	err = w.schedules().Req("GET", nil, &out)
	if err != nil {
		return
	}
	return out["schedules"], nil
}

// Schedule a Task
func (w *Worker) Schedule(schedules ...Schedule) (scheduleIds []string, err error) {
	outSchedules := make([]map[string]interface{}, 0, len(schedules))

	for _, schedule := range schedules {
		sm := map[string]interface{}{
			"code_name": schedule.CodeName,
			"name":      schedule.Name,
			"payload":   schedule.Payload,
			"label":     schedule.Label,
			"cluster":   schedule.Cluster,
		}
		if schedule.Delay != nil {
			sm["delay"] = (*schedule.Delay).Seconds()
		}
		if schedule.EndAt != nil {
			sm["end_at"] = *schedule.EndAt
		}
		if schedule.MaxConcurrency != nil {
			sm["max_concurrency"] = *schedule.MaxConcurrency
		}
		if schedule.Priority != nil {
			sm["priority"] = *schedule.Priority
		}
		if schedule.RunEvery != nil {
			sm["run_every"] = *schedule.RunEvery
		}
		if schedule.RunTimes != nil {
			sm["run_times"] = *schedule.RunTimes
		}
		if schedule.StartAt != nil {
			sm["start_at"] = *schedule.StartAt
		}
		outSchedules = append(outSchedules, sm)
	}

	in := map[string][]map[string]interface{}{"schedules": outSchedules}
	out := struct {
		Schedules []struct {
			Id string `json:"id"`
		} `json:"schedules"`
		Msg string `json:"msg"`
	}{}

	err = w.schedules().Req("POST", &in, &out)
	if err != nil {
		return
	}

	scheduleIds = make([]string, 0, len(out.Schedules))

	for _, schedule := range out.Schedules {
		scheduleIds = append(scheduleIds, schedule.Id)
	}

	return
}

// ScheduleInfo gets info about a scheduled task
func (w *Worker) ScheduleInfo(scheduleId string) (info ScheduleInfo, err error) {
	info = ScheduleInfo{}
	err = w.schedules(scheduleId).Req("GET", nil, &info)
	return info, nil
}

// ScheduleCancel cancels a scheduled task
func (w *Worker) ScheduleCancel(scheduleId string) (err error) {
	_, err = w.schedules(scheduleId, "cancel").Request("POST", nil)
	return
}
<|endoftext|>