New provider arukas (#11171)
* Add a Arukas provider * Add dependencies for the Arukas provider * Add documents for the Arukas
This commit is contained in:
parent
d5d5cd017c
commit
cd7f69ab11
|
@ -0,0 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform/builtin/providers/arukas"
|
||||
"github.com/hashicorp/terraform/plugin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
plugin.Serve(&plugin.ServeOpts{
|
||||
ProviderFunc: arukas.Provider,
|
||||
})
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package main
|
|
@ -0,0 +1,52 @@
|
|||
package arukas
|
||||
|
||||
import (
|
||||
API "github.com/arukasio/cli"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
JSONTokenParamName = "ARUKAS_JSON_API_TOKEN"
|
||||
JSONSecretParamName = "ARUKAS_JSON_API_SECRET"
|
||||
JSONUrlParamName = "ARUKAS_JSON_API_URL"
|
||||
JSONDebugParamName = "ARUKAS_DEBUG"
|
||||
JSONTimeoutParamName = "ARUKAS_TIMEOUT"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Token string
|
||||
Secret string
|
||||
URL string
|
||||
Trace string
|
||||
Timeout int
|
||||
}
|
||||
|
||||
func (c *Config) NewClient() (*ArukasClient, error) {
|
||||
|
||||
os.Setenv(JSONTokenParamName, c.Token)
|
||||
os.Setenv(JSONSecretParamName, c.Secret)
|
||||
os.Setenv(JSONUrlParamName, c.URL)
|
||||
os.Setenv(JSONDebugParamName, c.Trace)
|
||||
|
||||
client, err := API.NewClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.UserAgent = "Terraform for Arukas"
|
||||
|
||||
timeout := time.Duration(0)
|
||||
if c.Timeout > 0 {
|
||||
timeout = time.Duration(c.Timeout) * time.Second
|
||||
}
|
||||
|
||||
return &ArukasClient{
|
||||
Client: client,
|
||||
Timeout: timeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ArukasClient struct {
|
||||
*API.Client
|
||||
Timeout time.Duration
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package arukas
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
// Provider returns a terraform.ResourceProvider.
|
||||
func Provider() terraform.ResourceProvider {
|
||||
return &schema.Provider{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"token": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
DefaultFunc: schema.EnvDefaultFunc(JSONTokenParamName, nil),
|
||||
Description: "your Arukas APIKey(token)",
|
||||
},
|
||||
"secret": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
DefaultFunc: schema.EnvDefaultFunc(JSONSecretParamName, nil),
|
||||
Description: "your Arukas APIKey(secret)",
|
||||
},
|
||||
"api_url": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
DefaultFunc: schema.EnvDefaultFunc(JSONUrlParamName, "https://app.arukas.io/api/"),
|
||||
Description: "default Arukas API url",
|
||||
},
|
||||
"trace": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
DefaultFunc: schema.EnvDefaultFunc(JSONDebugParamName, ""),
|
||||
},
|
||||
"timeout": &schema.Schema{
|
||||
Type: schema.TypeInt,
|
||||
Optional: true,
|
||||
DefaultFunc: schema.EnvDefaultFunc(JSONTimeoutParamName, "600"),
|
||||
},
|
||||
},
|
||||
ResourcesMap: map[string]*schema.Resource{
|
||||
"arukas_container": resourceArukasContainer(),
|
||||
},
|
||||
ConfigureFunc: providerConfigure,
|
||||
}
|
||||
}
|
||||
|
||||
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
|
||||
|
||||
config := Config{
|
||||
Token: d.Get("token").(string),
|
||||
Secret: d.Get("secret").(string),
|
||||
URL: d.Get("api_url").(string),
|
||||
Trace: d.Get("trace").(string),
|
||||
Timeout: d.Get("timeout").(int),
|
||||
}
|
||||
|
||||
return config.NewClient()
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package arukas
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
var testAccProviders map[string]terraform.ResourceProvider
|
||||
var testAccProvider *schema.Provider
|
||||
|
||||
func init() {
|
||||
testAccProvider = Provider().(*schema.Provider)
|
||||
testAccProviders = map[string]terraform.ResourceProvider{
|
||||
"arukas": testAccProvider,
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider(t *testing.T) {
|
||||
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_impl(t *testing.T) {
|
||||
var _ terraform.ResourceProvider = Provider()
|
||||
}
|
||||
|
||||
func testAccPreCheck(t *testing.T) {
|
||||
if v := os.Getenv("ARUKAS_JSON_API_TOKEN"); v == "" {
|
||||
t.Fatal("ARUKAS_JSON_API_TOKEN must be set for acceptance tests")
|
||||
}
|
||||
if v := os.Getenv("ARUKAS_JSON_API_SECRET"); v == "" {
|
||||
t.Fatal("ARUKAS_JSON_API_SECRET must be set for acceptance tests")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,293 @@
|
|||
package arukas
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
API "github.com/arukasio/cli"
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func resourceArukasContainer() *schema.Resource {
|
||||
return &schema.Resource{
|
||||
Create: resourceArukasContainerCreate,
|
||||
Read: resourceArukasContainerRead,
|
||||
Update: resourceArukasContainerUpdate,
|
||||
Delete: resourceArukasContainerDelete,
|
||||
Importer: &schema.ResourceImporter{
|
||||
State: schema.ImportStatePassthrough,
|
||||
},
|
||||
Schema: map[string]*schema.Schema{
|
||||
"name": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
"image": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
"instances": &schema.Schema{
|
||||
Type: schema.TypeInt,
|
||||
Optional: true,
|
||||
Default: 1,
|
||||
ValidateFunc: validateIntegerInRange(1, 10),
|
||||
},
|
||||
"memory": &schema.Schema{
|
||||
Type: schema.TypeInt,
|
||||
Optional: true,
|
||||
Default: 256,
|
||||
ValidateFunc: validateIntInWord([]string{"256", "512"}),
|
||||
},
|
||||
"endpoint": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"ports": &schema.Schema{
|
||||
Type: schema.TypeList,
|
||||
Required: true,
|
||||
MaxItems: 20,
|
||||
Elem: &schema.Resource{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"protocol": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Default: "tcp",
|
||||
ValidateFunc: validateStringInWord([]string{"tcp", "udp"}),
|
||||
},
|
||||
"number": &schema.Schema{
|
||||
Type: schema.TypeInt,
|
||||
Optional: true,
|
||||
Default: "80",
|
||||
ValidateFunc: validateIntegerInRange(1, 65535),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"environments": &schema.Schema{
|
||||
Type: schema.TypeList,
|
||||
Optional: true,
|
||||
MaxItems: 20,
|
||||
Elem: &schema.Resource{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"key": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
"value": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"cmd": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
},
|
||||
"port_mappings": &schema.Schema{
|
||||
Type: schema.TypeList,
|
||||
Computed: true,
|
||||
Elem: &schema.Resource{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"host": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
},
|
||||
"ipaddress": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
},
|
||||
"container_port": &schema.Schema{
|
||||
Type: schema.TypeInt,
|
||||
Computed: true,
|
||||
},
|
||||
"service_port": &schema.Schema{
|
||||
Type: schema.TypeInt,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"endpoint_full_hostname": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
},
|
||||
"endpoint_full_url": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
},
|
||||
"app_id": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func resourceArukasContainerCreate(d *schema.ResourceData, meta interface{}) error {
|
||||
client := meta.(*ArukasClient)
|
||||
|
||||
var appSet API.AppSet
|
||||
|
||||
// create an app
|
||||
newApp := API.App{Name: d.Get("name").(string)}
|
||||
|
||||
var parsedEnvs API.Envs
|
||||
var parsedPorts API.Ports
|
||||
|
||||
if rawEnvs, ok := d.GetOk("environments"); ok {
|
||||
parsedEnvs = expandEnvs(rawEnvs)
|
||||
}
|
||||
if rawPorts, ok := d.GetOk("ports"); ok {
|
||||
parsedPorts = expandPorts(rawPorts)
|
||||
}
|
||||
|
||||
newContainer := API.Container{
|
||||
Envs: parsedEnvs,
|
||||
Ports: parsedPorts,
|
||||
ImageName: d.Get("image").(string),
|
||||
Mem: d.Get("memory").(int),
|
||||
Instances: d.Get("instances").(int),
|
||||
Cmd: d.Get("cmd").(string),
|
||||
|
||||
Name: d.Get("endpoint").(string),
|
||||
}
|
||||
newAppSet := API.AppSet{
|
||||
App: newApp,
|
||||
Container: newContainer,
|
||||
}
|
||||
|
||||
// create
|
||||
if err := client.Post(&appSet, "/app-sets", newAppSet); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start container
|
||||
if err := client.Post(nil, fmt.Sprintf("/containers/%s/power", appSet.Container.ID), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sleepUntilUp(client, appSet.Container.ID, client.Timeout); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.SetId(appSet.Container.ID)
|
||||
return resourceArukasContainerRead(d, meta)
|
||||
}
|
||||
|
||||
func resourceArukasContainerRead(d *schema.ResourceData, meta interface{}) error {
|
||||
client := meta.(*ArukasClient)
|
||||
|
||||
var container API.Container
|
||||
var app API.App
|
||||
|
||||
if err := client.Get(&container, fmt.Sprintf("/containers/%s", d.Id())); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := client.Get(&app, fmt.Sprintf("/apps/%s", container.AppID)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.Set("app_id", container.AppID)
|
||||
d.Set("name", app.Name)
|
||||
d.Set("image", container.ImageName)
|
||||
d.Set("instances", container.Instances)
|
||||
d.Set("memory", container.Mem)
|
||||
endpoint := container.Endpoint
|
||||
if strings.HasSuffix(endpoint, ".arukascloud.io") {
|
||||
endpoint = strings.Replace(endpoint, ".arukascloud.io", "", -1)
|
||||
}
|
||||
|
||||
d.Set("endpoint", endpoint)
|
||||
d.Set("endpoint_full_hostname", container.Endpoint)
|
||||
d.Set("endpoint_full_url", fmt.Sprintf("https://%s", container.Endpoint))
|
||||
|
||||
d.Set("cmd", container.Cmd)
|
||||
|
||||
//ports
|
||||
d.Set("ports", flattenPorts(container.Ports))
|
||||
|
||||
//port mappings
|
||||
d.Set("port_mappings", flattenPortMappings(container.PortMappings))
|
||||
|
||||
//envs
|
||||
d.Set("environments", flattenEnvs(container.Envs))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceArukasContainerUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||
|
||||
client := meta.(*ArukasClient)
|
||||
var container API.Container
|
||||
|
||||
if err := client.Get(&container, fmt.Sprintf("/containers/%s", d.Id())); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var parsedEnvs API.Envs
|
||||
var parsedPorts API.Ports
|
||||
|
||||
if rawEnvs, ok := d.GetOk("environments"); ok {
|
||||
parsedEnvs = expandEnvs(rawEnvs)
|
||||
}
|
||||
if rawPorts, ok := d.GetOk("ports"); ok {
|
||||
parsedPorts = expandPorts(rawPorts)
|
||||
}
|
||||
|
||||
newContainer := API.Container{
|
||||
Envs: parsedEnvs,
|
||||
Ports: parsedPorts,
|
||||
ImageName: d.Get("image").(string),
|
||||
Mem: d.Get("memory").(int),
|
||||
Instances: d.Get("instances").(int),
|
||||
Cmd: d.Get("cmd").(string),
|
||||
Name: d.Get("endpoint").(string),
|
||||
}
|
||||
|
||||
// update
|
||||
if err := client.Patch(nil, fmt.Sprintf("/containers/%s", d.Id()), newContainer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return resourceArukasContainerRead(d, meta)
|
||||
|
||||
}
|
||||
|
||||
func resourceArukasContainerDelete(d *schema.ResourceData, meta interface{}) error {
|
||||
client := meta.(*ArukasClient)
|
||||
var container API.Container
|
||||
|
||||
if err := client.Get(&container, fmt.Sprintf("/containers/%s", d.Id())); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := client.Delete(fmt.Sprintf("/apps/%s", container.AppID)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sleepUntilUp(client *ArukasClient, containerID string, timeout time.Duration) error {
|
||||
current := 0 * time.Second
|
||||
interval := 5 * time.Second
|
||||
for {
|
||||
var container API.Container
|
||||
if err := client.Get(&container, fmt.Sprintf("/containers/%s", containerID)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if container.IsRunning {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(interval)
|
||||
current += interval
|
||||
|
||||
if timeout > 0 && current > timeout {
|
||||
return fmt.Errorf("Timeout: sleepUntilUp")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
package arukas
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
API "github.com/arukasio/cli"
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAccArukasContainer_Basic(t *testing.T) {
|
||||
var container API.Container
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
CheckDestroy: testAccCheckArukasContainerDestroy,
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAccCheckArukasContainerConfig_basic,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccCheckArukasContainerExists("arukas_container.foobar", &container),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "name", "terraform_for_arukas_test_foobar"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "image", "nginx:latest"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "instances", "1"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "memory", "256"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "endpoint", "terraform-for-arukas-test-endpoint"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "ports.#", "1"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "ports.0.protocol", "tcp"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "ports.0.number", "80"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "environments.#", "1"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "environments.0.key", "key"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "environments.0.value", "value"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "port_mappings.#", "1"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccArukasContainer_Update(t *testing.T) {
|
||||
var container API.Container
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
CheckDestroy: testAccCheckArukasContainerDestroy,
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAccCheckArukasContainerConfig_basic,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccCheckArukasContainerExists("arukas_container.foobar", &container),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "name", "terraform_for_arukas_test_foobar"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "image", "nginx:latest"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "instances", "1"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "memory", "256"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "endpoint", "terraform-for-arukas-test-endpoint"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "ports.#", "1"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "ports.0.protocol", "tcp"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "ports.0.number", "80"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "environments.#", "1"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "environments.0.key", "key"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "environments.0.value", "value"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "port_mappings.#", "1"),
|
||||
),
|
||||
},
|
||||
resource.TestStep{
|
||||
Config: testAccCheckArukasContainerConfig_update,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccCheckArukasContainerExists("arukas_container.foobar", &container),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "name", "terraform_for_arukas_test_foobar_upd"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "image", "nginx:latest"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "instances", "2"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "memory", "512"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "endpoint", "terraform-for-arukas-test-endpoint-upd"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "ports.#", "2"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "ports.0.protocol", "tcp"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "ports.0.number", "80"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "ports.1.protocol", "tcp"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "ports.1.number", "443"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "environments.#", "2"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "environments.0.key", "key"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "environments.0.value", "value"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "environments.1.key", "key_upd"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "environments.1.value", "value_upd"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "port_mappings.#", "4"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccArukasContainer_Minimum(t *testing.T) {
|
||||
var container API.Container
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
CheckDestroy: testAccCheckArukasContainerDestroy,
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAccCheckArukasContainerConfig_minimum,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccCheckArukasContainerExists("arukas_container.foobar", &container),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "name", "terraform_for_arukas_test_foobar"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "image", "nginx:latest"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "instances", "1"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "memory", "256"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "ports.#", "1"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "ports.0.protocol", "tcp"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "ports.0.number", "80"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"arukas_container.foobar", "port_mappings.#", "1"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccArukasContainer_Import(t *testing.T) {
|
||||
resourceName := "arukas_container.foobar"
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
CheckDestroy: testAccCheckArukasContainerDestroy,
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAccCheckArukasContainerConfig_basic,
|
||||
},
|
||||
resource.TestStep{
|
||||
ResourceName: resourceName,
|
||||
ImportState: true,
|
||||
ImportStateVerify: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccCheckArukasContainerExists(n string, container *API.Container) resource.TestCheckFunc {
|
||||
return func(s *terraform.State) error {
|
||||
rs, ok := s.RootModule().Resources[n]
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("Not found: %s", n)
|
||||
}
|
||||
|
||||
if rs.Primary.ID == "" {
|
||||
return fmt.Errorf("No Container ID is set")
|
||||
}
|
||||
client := testAccProvider.Meta().(*ArukasClient)
|
||||
var foundContainer API.Container
|
||||
err := client.Get(&foundContainer, fmt.Sprintf("/containers/%s", rs.Primary.ID))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if foundContainer.ID != rs.Primary.ID {
|
||||
return fmt.Errorf("Container not found")
|
||||
}
|
||||
|
||||
*container = foundContainer
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testAccCheckArukasContainerDestroy(s *terraform.State) error {
|
||||
client := testAccProvider.Meta().(*ArukasClient)
|
||||
|
||||
for _, rs := range s.RootModule().Resources {
|
||||
if rs.Type != "arukas_container" {
|
||||
continue
|
||||
}
|
||||
|
||||
err := client.Get(nil, fmt.Sprintf("/containers/%s", rs.Primary.ID))
|
||||
|
||||
if err == nil {
|
||||
return fmt.Errorf("Note still exists")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const testAccCheckArukasContainerConfig_basic = `
|
||||
resource "arukas_container" "foobar" {
|
||||
name = "terraform_for_arukas_test_foobar"
|
||||
image = "nginx:latest"
|
||||
instances = 1
|
||||
memory = 256
|
||||
endpoint = "terraform-for-arukas-test-endpoint"
|
||||
ports = {
|
||||
protocol = "tcp"
|
||||
number = "80"
|
||||
}
|
||||
environments {
|
||||
key = "key"
|
||||
value = "value"
|
||||
}
|
||||
}`
|
||||
|
||||
const testAccCheckArukasContainerConfig_update = `
|
||||
resource "arukas_container" "foobar" {
|
||||
name = "terraform_for_arukas_test_foobar_upd"
|
||||
image = "nginx:latest"
|
||||
instances = 2
|
||||
memory = 512
|
||||
endpoint = "terraform-for-arukas-test-endpoint-upd"
|
||||
ports = {
|
||||
protocol = "tcp"
|
||||
number = "80"
|
||||
}
|
||||
ports = {
|
||||
protocol = "tcp"
|
||||
number = "443"
|
||||
}
|
||||
environments {
|
||||
key = "key"
|
||||
value = "value"
|
||||
}
|
||||
environments {
|
||||
key = "key_upd"
|
||||
value = "value_upd"
|
||||
}
|
||||
}`
|
||||
|
||||
const testAccCheckArukasContainerConfig_minimum = `
|
||||
resource "arukas_container" "foobar" {
|
||||
name = "terraform_for_arukas_test_foobar"
|
||||
image = "nginx:latest"
|
||||
ports = {
|
||||
number = "80"
|
||||
}
|
||||
}`
|
|
@ -0,0 +1,110 @@
|
|||
package arukas
|
||||
|
||||
import (
|
||||
API "github.com/arukasio/cli"
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Takes the result of flatmap.Expand for an array of strings
|
||||
// and returns a []string
|
||||
func expandStringList(configured []interface{}) []string {
|
||||
vs := make([]string, 0, len(configured))
|
||||
for _, v := range configured {
|
||||
vs = append(vs, string(v.(string)))
|
||||
}
|
||||
return vs
|
||||
}
|
||||
|
||||
// Takes the result of schema.Set of strings and returns a []string
|
||||
func expandStringSet(configured *schema.Set) []string {
|
||||
return expandStringList(configured.List())
|
||||
}
|
||||
|
||||
// Takes list of pointers to strings. Expand to an array
|
||||
// of raw strings and returns a []interface{}
|
||||
// to keep compatibility w/ schema.NewSetschema.NewSet
|
||||
func flattenStringList(list []string) []interface{} {
|
||||
vs := make([]interface{}, 0, len(list))
|
||||
for _, v := range list {
|
||||
vs = append(vs, v)
|
||||
}
|
||||
return vs
|
||||
}
|
||||
|
||||
func expandEnvs(configured interface{}) API.Envs {
|
||||
var envs API.Envs
|
||||
if configured == nil {
|
||||
return envs
|
||||
}
|
||||
rawEnvs := configured.([]interface{})
|
||||
for _, raw := range rawEnvs {
|
||||
env := raw.(map[string]interface{})
|
||||
envs = append(envs, API.Env{Key: env["key"].(string), Value: env["value"].(string)})
|
||||
}
|
||||
return envs
|
||||
}
|
||||
|
||||
func flattenEnvs(envs API.Envs) []interface{} {
|
||||
var ret []interface{}
|
||||
for _, env := range envs {
|
||||
r := map[string]interface{}{}
|
||||
r["key"] = env.Key
|
||||
r["value"] = env.Value
|
||||
ret = append(ret, r)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func expandPorts(configured interface{}) API.Ports {
|
||||
var ports API.Ports
|
||||
if configured == nil {
|
||||
return ports
|
||||
}
|
||||
rawPorts := configured.([]interface{})
|
||||
for _, raw := range rawPorts {
|
||||
port := raw.(map[string]interface{})
|
||||
ports = append(ports, API.Port{Protocol: port["protocol"].(string), Number: port["number"].(int)})
|
||||
}
|
||||
return ports
|
||||
}
|
||||
|
||||
func flattenPorts(ports API.Ports) []interface{} {
|
||||
var ret []interface{}
|
||||
for _, port := range ports {
|
||||
r := map[string]interface{}{}
|
||||
r["protocol"] = port.Protocol
|
||||
r["number"] = port.Number
|
||||
ret = append(ret, r)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
func flattenPortMappings(ports API.PortMappings) []interface{} {
|
||||
var ret []interface{}
|
||||
for _, tasks := range ports {
|
||||
for _, port := range tasks {
|
||||
r := map[string]interface{}{}
|
||||
ip := ""
|
||||
|
||||
addrs, err := net.LookupHost(port.Host)
|
||||
if err == nil && len(addrs) > 0 {
|
||||
ip = addrs[0]
|
||||
}
|
||||
|
||||
r["host"] = port.Host
|
||||
r["ipaddress"] = ip
|
||||
r["container_port"] = port.ContainerPort
|
||||
r["service_port"] = port.ServicePort
|
||||
ret = append(ret, r)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func forceString(target interface{}) string {
|
||||
if target == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return target.(string)
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package arukas
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func validateMaxLength(minLength, maxLength int) schema.SchemaValidateFunc {
|
||||
return func(v interface{}, k string) (ws []string, errors []error) {
|
||||
value := v.(string)
|
||||
if len(value) < minLength {
|
||||
errors = append(errors, fmt.Errorf(
|
||||
"%q cannot be shorter than %d characters: %q", k, minLength, value))
|
||||
}
|
||||
if len(value) > maxLength {
|
||||
errors = append(errors, fmt.Errorf(
|
||||
"%q cannot be longer than %d characters: %q", k, maxLength, value))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func validateIntegerInRange(min, max int) schema.SchemaValidateFunc {
|
||||
return func(v interface{}, k string) (ws []string, errors []error) {
|
||||
value := v.(int)
|
||||
if value < min {
|
||||
errors = append(errors, fmt.Errorf(
|
||||
"%q cannot be lower than %d: %d", k, min, value))
|
||||
}
|
||||
if value > max {
|
||||
errors = append(errors, fmt.Errorf(
|
||||
"%q cannot be higher than %d: %d", k, max, value))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func validateStringInWord(allowWords []string) schema.SchemaValidateFunc {
|
||||
return func(v interface{}, k string) (ws []string, errors []error) {
|
||||
var found bool
|
||||
for _, t := range allowWords {
|
||||
if v.(string) == t {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
errors = append(errors, fmt.Errorf("%q must be one of [%s]", k, strings.Join(allowWords, "/")))
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func validateIntInWord(allowWords []string) schema.SchemaValidateFunc {
|
||||
return func(v interface{}, k string) (ws []string, errors []error) {
|
||||
var found bool
|
||||
for _, t := range allowWords {
|
||||
if fmt.Sprintf("%d", v.(int)) == t {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
errors = append(errors, fmt.Errorf("%q must be one of [%s]", k, strings.Join(allowWords, "/")))
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func validateDNSRecordValue() schema.SchemaValidateFunc {
|
||||
return func(v interface{}, k string) (ws []string, errors []error) {
|
||||
var rtype, value string
|
||||
|
||||
values := v.(map[string]interface{})
|
||||
rtype = values["type"].(string)
|
||||
value = values["value"].(string)
|
||||
switch rtype {
|
||||
case "MX", "NS", "CNAME":
|
||||
if rtype == "MX" {
|
||||
if values["priority"] == nil {
|
||||
errors = append(errors, fmt.Errorf("%q required when TYPE was MX", k))
|
||||
}
|
||||
}
|
||||
if !strings.HasSuffix(value, ".") {
|
||||
errors = append(errors, fmt.Errorf("%q must be period at the end [%s]", k, value))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
}
|
|
@ -8,6 +8,7 @@ package command
|
|||
import (
|
||||
alicloudprovider "github.com/hashicorp/terraform/builtin/providers/alicloud"
|
||||
archiveprovider "github.com/hashicorp/terraform/builtin/providers/archive"
|
||||
arukasprovider "github.com/hashicorp/terraform/builtin/providers/arukas"
|
||||
atlasprovider "github.com/hashicorp/terraform/builtin/providers/atlas"
|
||||
awsprovider "github.com/hashicorp/terraform/builtin/providers/aws"
|
||||
azureprovider "github.com/hashicorp/terraform/builtin/providers/azure"
|
||||
|
@ -79,6 +80,7 @@ import (
|
|||
var InternalProviders = map[string]plugin.ProviderFunc{
|
||||
"alicloud": alicloudprovider.Provider,
|
||||
"archive": archiveprovider.Provider,
|
||||
"arukas": arukasprovider.Provider,
|
||||
"atlas": atlasprovider.Provider,
|
||||
"aws": awsprovider.Provider,
|
||||
"azure": azureprovider.Provider,
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
# Building Arukas CLI
|
||||
|
||||
This document contains details about the process for building binaries for Arukas CLI
|
||||
|
||||
## QuickBuild
|
||||
|
||||
**Please note: Replaced by your arukas token and aruaks api secret is
|
||||
`YOUR_API_TOKEN` and `YOUR_API_SECRET`**
|
||||
|
||||
* Clone the repo: `git clone https://github.com/arukasio/cli.git`
|
||||
* CLI Build: `docker build -t arukasio/arukas:patch .`
|
||||
* Test execute the CLI: `docker run --rm -e ARUKAS_JSON_API_TOKEN="YOUR_API_TOKEN"
|
||||
-e ARUKAS_JSON_API_SECRET="YOUR_API_SECRET" arukasio/arukas:patch`
|
||||
|
||||
### Godep
|
||||
|
||||
You can use the `godep` in order to install the external package that depends.
|
||||
It will install the package versions specified in `Godeps/Godeps.json` to your `$GOPATH`
|
||||
|
||||
```
|
||||
go get -u github.com/tools/godep
|
||||
godep restore
|
||||
```
|
||||
|
||||
## Cross Compilation and Building for Distribution
|
||||
|
||||
If you wish to cross-compile arukas-cli for another architecture, you can set the `XC_OS` and `XC_ARCH` environment variables to values representing the target operating system and architecture before calling `make`. The output is placed in the `pkg` subdirectory tree both expanded in a directory representing the OS/architecture combination and as a ZIP archive.
|
||||
|
||||
For example, to compile 64-bit Linux binaries on Mac OS X Linux, you can run:
|
||||
|
||||
```sh
|
||||
$ XC_OS=linux XC_ARCH=amd64 make bin
|
||||
...
|
||||
$ file pkg/linux_amd64/arukas
|
||||
arukas: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
|
||||
```
|
||||
|
||||
`XC_OS` and `XC_ARCH` can be space separated lists representing different combinations of operating system and architecture. For example, to compile for both Linux and Mac OS X, targeting both 32- and 64-bit architectures, you can run:
|
||||
|
||||
```sh
|
||||
$ XC_OS="linux darwin" XC_ARCH="386 amd64" make bin
|
||||
...
|
||||
$ tree ./pkg/ -P "arukas|*.zip"
|
||||
./pkg/
|
||||
├── darwin_386
|
||||
│ └── arukas
|
||||
├── darwin_386.zip
|
||||
├── darwin_amd64
|
||||
│ └── arukas
|
||||
├── darwin_amd64.zip
|
||||
├── linux_386
|
||||
│ └── arukas
|
||||
├── linux_386.zip
|
||||
├── linux_amd64
|
||||
│ └── arukas
|
||||
└── linux_amd64.zip
|
||||
|
||||
4 directories, 8 files
|
||||
```
|
||||
|
||||
_Note: Cross-compilation uses [gox](https://github.com/mitchellh/gox), which requires toolchains to be built with versions of Go prior to 1.5. In order to successfully cross-compile with older versions of Go, you will need to run `gox -build-toolchain` before running the commands detailed above._
|
|
@ -0,0 +1,15 @@
|
|||
FROM arukasio/arukas:dev
|
||||
MAINTAINER "Shuji Yamada <s-yamada@arukas.io>"
|
||||
|
||||
ENV REPO_ROOT $GOPATH/src/github.com/arukasio/cli
|
||||
|
||||
COPY . $REPO_ROOT
|
||||
WORKDIR $REPO_ROOT
|
||||
|
||||
RUN godep restore
|
||||
RUN for package in $(go list ./...| grep -v vendor); do golint ${package}; done
|
||||
RUN ARUKAS_DEV=1 scripts/build.sh
|
||||
|
||||
WORKDIR $GOPATH
|
||||
|
||||
ENTRYPOINT ["bin/arukas"]
|
|
@ -0,0 +1,21 @@
|
|||
Copyright (C) 2015 Arukas
|
||||
All Rights Reserved.
|
||||
|
||||
MIT LICENSE
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,71 @@
|
|||
TEST?=$$(go list ./... | grep -v /vendor/)
|
||||
VETARGS?=-all
|
||||
GOFMT_FILES?=$$(find . -name '*.go' | grep -v vendor)
|
||||
|
||||
default: vet
|
||||
|
||||
# bin generates the releaseable binaries for Arukas
|
||||
bin: fmtcheck generate
|
||||
@sh -c "'$(CURDIR)/scripts/build.sh'"
|
||||
|
||||
# dev creates binaries for testing Arukas locally. These are put
|
||||
# into ./bin/ as well as $GOPATH/bin
|
||||
dev: fmtcheck generate
|
||||
@ARUKAS_DEV=1 sh -c "'$(CURDIR)/scripts/build.sh'"
|
||||
|
||||
quickdev: generate
|
||||
@ARUKAS_QUICKDEV=1 ARUKAS_DEV=1 sh -c "'$(CURDIR)/scripts/build.sh'"
|
||||
|
||||
# Shorthand for quickly building the core of Arukas. Note that some
|
||||
# changes will require a rebuild of everything, in which case the dev
|
||||
# target should be used.
|
||||
core-dev: fmtcheck generate
|
||||
go install github.com/arukasio/cli
|
||||
|
||||
# Shorthand for quickly testing the core of Arukas (i.e. "not providers")
|
||||
core-test: generate
|
||||
@echo "Testing core packages..." && go test $(shell go list ./... | grep -v -E 'builtin|vendor')
|
||||
|
||||
# Shorthand for building and installing just one plugin for local testing.
|
||||
# Run as (for example): make plugin-dev PLUGIN=provider-aws
|
||||
plugin-dev: fmtcheck generate
|
||||
go install github.com/hashicorp/terraform/builtin/bins/$(PLUGIN)
|
||||
mv $(GOPATH)/bin/$(PLUGIN) $(GOPATH)/bin/terraform-$(PLUGIN)
|
||||
|
||||
# test runs the unit tests
|
||||
test: fmtcheck generate
|
||||
ARUKAS_ACC= go test $(TEST) $(TESTARGS) -timeout=30s -parallel=4
|
||||
|
||||
# testrace runs the race checker
|
||||
testrace: fmtcheck generate
|
||||
ARUKAS_ACC= go test -race $(TEST) $(TESTARGS)
|
||||
|
||||
# vet runs the Go source code static analysis tool `vet` to find
|
||||
# any common errors.
|
||||
vet:
|
||||
@go tool vet 2>/dev/null ; if [ $$? -eq 3 ]; then \
|
||||
go get golang.org/x/tools/cmd/vet; \
|
||||
fi
|
||||
@echo "go tool vet $(VETARGS) ."
|
||||
@go tool vet $(VETARGS) $$(ls -d */ | grep -v vendor) ; if [ $$? -eq 1 ]; then \
|
||||
echo ""; \
|
||||
echo "Vet found suspicious constructs. Please check the reported constructs"; \
|
||||
echo "and fix them if necessary before submitting the code for review."; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# generate runs `go generate` to build the dynamically generated
|
||||
# source files.
|
||||
generate:
|
||||
@which stringer ; if [ $$? -ne 0 ]; then \
|
||||
go get -u golang.org/x/tools/cmd/stringer; \
|
||||
fi
|
||||
go generate $$(go list ./... | grep -v /vendor/)
|
||||
|
||||
fmt:
|
||||
gofmt -w $(GOFMT_FILES)
|
||||
|
||||
fmtcheck:
|
||||
@sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'"
|
||||
|
||||
.PHONY: bin default generate test updatedeps vet fmt fmtcheck
|
|
@ -0,0 +1,37 @@
|
|||
<img src="https://app.arukas.io/images/logo-orca.svg" alt="" width="100" /> Arukas CLI
|
||||
==========
|
||||
|
||||
[![Circle CI](https://circleci.com/gh/arukasio/cli.svg?style=shield)](https://circleci.com/gh/arukasio/cli)
|
||||
|
||||
The Arukas CLI is used to manage Arukas apps from the command line.
|
||||
* Website: https://arukas.io
|
||||
|
||||
### Binary Releases
|
||||
|
||||
The official binary of Arukas CLI: https://github.com/arukasio/cli/releases/
|
||||
|
||||
### Dockerized
|
||||
|
||||
A dockerized version of Arukas CLI: https://hub.docker.com/r/arukasio/arukas/
|
||||
|
||||
## Setup
|
||||
|
||||
* Get API key here: https://app.arukas.io/settings/api-keys
|
||||
* Edit it `.env` file
|
||||
|
||||
You can overload and customize specific variables when running scripts.
|
||||
|
||||
Simply create `.env` with the environment variables you need,
|
||||
for example, `ARUKAS_JSON_API_TOKEN` and `ARUKAS_JSON_API_SECRET`
|
||||
|
||||
```
|
||||
# .env
|
||||
ARUKAS_JSON_API_TOKEN=YOUR_API_TOKEN
|
||||
ARUKAS_JSON_API_SECRET=YOUR_API_SECRET
|
||||
```
|
||||
|
||||
You can look at `.env.sample` for other variables used by this application.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the terms of the MIT license.
|
|
@ -0,0 +1,65 @@
|
|||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
|
||||
VAGRANTFILE_API_VERSION = "2"
|
||||
|
||||
$script = <<SCRIPT
|
||||
GOVERSION="1.5.2"
|
||||
SRCROOT="/opt/go"
|
||||
SRCPATH="/opt/gopath"
|
||||
|
||||
# Get the ARCH
|
||||
ARCH=`uname -m | sed 's|i686|386|' | sed 's|x86_64|amd64|'`
|
||||
|
||||
# Install Prereq Packages
|
||||
sudo apt-get update
|
||||
sudo apt-get upgrade -y
|
||||
sudo apt-get install -y build-essential curl git-core libpcre3-dev mercurial pkg-config zip tree shellcheck
|
||||
|
||||
# Install Go
|
||||
cd /tmp
|
||||
wget --quiet https://storage.googleapis.com/golang/go${GOVERSION}.linux-${ARCH}.tar.gz
|
||||
tar -xvf go${GOVERSION}.linux-${ARCH}.tar.gz
|
||||
sudo mv go $SRCROOT
|
||||
sudo chmod 775 $SRCROOT
|
||||
sudo chown vagrant:vagrant $SRCROOT
|
||||
|
||||
# Setup the GOPATH; even though the shared folder spec gives the working
|
||||
# directory the right user/group, we need to set it properly on the
|
||||
# parent path to allow subsequent "go get" commands to work.
|
||||
sudo mkdir -p $SRCPATH
|
||||
sudo chown -R vagrant:vagrant $SRCPATH 2>/dev/null || true
|
||||
# ^^ silencing errors here because we expect this to fail for the shared folder
|
||||
|
||||
cat <<EOF >/tmp/gopath.sh
|
||||
export GOPATH="$SRCPATH"
|
||||
export GOROOT="$SRCROOT"
|
||||
export PATH="$SRCROOT/bin:$SRCPATH/bin:\$PATH"
|
||||
export GO15VENDOREXPERIMENT=1
|
||||
EOF
|
||||
|
||||
sudo mv /tmp/gopath.sh /etc/profile.d/gopath.sh
|
||||
sudo chmod 0755 /etc/profile.d/gopath.sh
|
||||
source /etc/profile.d/gopath.sh
|
||||
SCRIPT
|
||||
|
||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||
config.vm.box = "bento/ubuntu-14.04"
|
||||
config.vm.hostname = "cli"
|
||||
|
||||
config.vm.provision "shell", inline: $script, privileged: false
|
||||
config.vm.synced_folder '.', '/opt/gopath/src/github.com/arukasio/cli'
|
||||
|
||||
["vmware_fusion", "vmware_workstation"].each do |p|
|
||||
config.vm.provider p do |v|
|
||||
v.vmx["memsize"] = "1024"
|
||||
v.vmx["numvcpus"] = "2"
|
||||
end
|
||||
end
|
||||
|
||||
config.vm.provider "virtualbox" do |v|
|
||||
v.memory = 2048
|
||||
v.cpus = 2
|
||||
end
|
||||
end
|
|
@ -0,0 +1,114 @@
|
|||
package arukas
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/manyminds/api2go/jsonapi"
|
||||
)
|
||||
|
||||
// TmpJSON Contain JSON data.
|
||||
type TmpJSON struct {
|
||||
Data []map[string]interface{} `json:"data"`
|
||||
Meta map[string]interface{} `json:"-"`
|
||||
}
|
||||
|
||||
// AppSet represents a application data in struct variables.
|
||||
type AppSet struct {
|
||||
App App
|
||||
Container Container
|
||||
}
|
||||
|
||||
// MarshalJSON returns as as the JSON encoding of as.
|
||||
func (as AppSet) MarshalJSON() ([]byte, error) {
|
||||
var (
|
||||
app []byte
|
||||
appJSON map[string]map[string]interface{}
|
||||
container []byte
|
||||
containerJSON map[string]map[string]interface{}
|
||||
marshaled []byte
|
||||
err error
|
||||
)
|
||||
|
||||
if app, err = jsonapi.Marshal(as.App); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(app, &appJSON); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if container, err = jsonapi.Marshal(as.Container); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(container, &containerJSON); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := map[string][]map[string]interface{}{
|
||||
"data": []map[string]interface{}{
|
||||
appJSON["data"],
|
||||
containerJSON["data"],
|
||||
},
|
||||
}
|
||||
|
||||
if marshaled, err = json.Marshal(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return marshaled, nil
|
||||
}
|
||||
|
||||
// SelectResources returns the type filter value of TmpJSON.
|
||||
func SelectResources(data TmpJSON, resourceType string) map[string][]map[string]interface{} {
|
||||
var resources []map[string]interface{}
|
||||
// resources := make([]map[string]interface{}, 0)
|
||||
|
||||
for _, v := range data.Data {
|
||||
if v["type"] == resourceType {
|
||||
resources = append(resources, v)
|
||||
}
|
||||
}
|
||||
|
||||
filtered := map[string][]map[string]interface{}{
|
||||
"data": resources,
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// UnmarshalJSON sets *as to a copy of data.
|
||||
func (as *AppSet) UnmarshalJSON(bytes []byte) error {
|
||||
var (
|
||||
appBytes []byte
|
||||
containerBytes []byte
|
||||
err error
|
||||
data TmpJSON
|
||||
)
|
||||
if err = json.Unmarshal(bytes, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apps := SelectResources(data, "apps")
|
||||
containers := SelectResources(data, "containers")
|
||||
|
||||
if appBytes, err = json.Marshal(apps); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if containerBytes, err = json.Marshal(containers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var parsedApps []App
|
||||
if err = jsonapi.Unmarshal(appBytes, &parsedApps); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var parsedContainers []Container
|
||||
if err = jsonapi.Unmarshal(containerBytes, &parsedContainers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
as.App = parsedApps[0]
|
||||
as.Container = parsedContainers[0]
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package arukas
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// App represents a application data in struct variables.
|
||||
type App struct {
|
||||
ID string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"-"`
|
||||
ContainerID string `json:"-"`
|
||||
Container *Container `json:"-"`
|
||||
User *User `json:"-"`
|
||||
}
|
||||
|
||||
// GetID returns a stringified of an ID.
|
||||
func (a App) GetID() string {
|
||||
return string(a.ID)
|
||||
}
|
||||
|
||||
// SetID to satisfy jsonapi.UnmarshalIdentifier interface.
|
||||
func (a *App) SetID(ID string) error {
|
||||
a.ID = ID
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetToOneReferenceID sets the reference ID and satisfies the jsonapi.UnmarshalToOneRelations interface
|
||||
func (a *App) SetToOneReferenceID(name, ID string) error {
|
||||
if name == "container" {
|
||||
if ID == "" {
|
||||
a.Container = nil
|
||||
} else {
|
||||
a.Container = &Container{ID: ID}
|
||||
}
|
||||
|
||||
return nil
|
||||
} else if name == "user" {
|
||||
if ID == "" {
|
||||
a.User = nil
|
||||
} else {
|
||||
a.User = &User{ID: ID}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("There is no to-one relationship with the name " + name)
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
machine:
|
||||
environment:
|
||||
GODIST: "go1.7.3.linux-amd64.tar.gz"
|
||||
GOPATH: /home/ubuntu/.go_workspace
|
||||
ARUKAS_JSON_API_SECRET: PASSWORD
|
||||
ARUKAS_JSON_API_TOKEN: USER
|
||||
REPO_ROOT: /home/ubuntu/.go_workspace/src/github.com/arukasio/cli
|
||||
|
||||
dependencies:
|
||||
cache_directories:
|
||||
- /home/ubuntu/.go_workspace
|
||||
pre:
|
||||
- if [[ ! -e /home/ubuntu/go/bin/go ]]; then cd /home/ubuntu; curl https://storage.googleapis.com/golang/${GODIST} | tar -xz; fi
|
||||
- sudo rm -rf /usr/local/go
|
||||
- sudo mv /home/ubuntu/go /usr/local/go
|
||||
- go get -u github.com/tools/godep
|
||||
- go get -u github.com/golang/lint/golint
|
||||
override:
|
||||
- mkdir -p ${REPO_ROOT}
|
||||
- rsync -azC --delete ./ ${REPO_ROOT}
|
||||
|
||||
test:
|
||||
pre:
|
||||
- cd ${REPO_ROOT} && godep restore
|
||||
override:
|
||||
- cd ${REPO_ROOT} && make test vet
|
||||
- cd ${REPO_ROOT} && for package in `go list ./...| grep -v vendor`; do golint ${package}; done
|
||||
- cd ${REPO_ROOT} && godep go test -cover -bench -benchmem `go list ./... | grep -v /vendor/` -v
|
||||
|
||||
deployment:
|
||||
release:
|
||||
tag: /v[0-9]+(\.[0-9]+)*/
|
||||
commands:
|
||||
- cd ${REPO_ROOT} && CGO_ENABLED=0 XC_OS="linux darwin windows" XC_ARCH="amd64" make bin
|
||||
- cd ${REPO_ROOT} && test "${CIRCLE_TAG}" == "$(arukas version)"
|
||||
- cd ${REPO_ROOT} && bash ./scripts/dist.sh "$(arukas version)"
|
|
@ -0,0 +1,324 @@
|
|||
package arukas
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/manyminds/api2go/jsonapi"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// VERSION is cli version.
|
||||
const VERSION = "v0.1.3"
|
||||
|
||||
// Client represents a user client data in struct variables.
|
||||
type Client struct {
|
||||
APIURL *url.URL
|
||||
HTTP *http.Client
|
||||
Username string
|
||||
Password string
|
||||
UserAgent string
|
||||
Debug bool
|
||||
Output func(...interface{})
|
||||
OutputDest io.Writer
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
var (
|
||||
client *Client
|
||||
)
|
||||
|
||||
// PrintHeaderln print as the values.
|
||||
func (c *Client) PrintHeaderln(values ...interface{}) {
|
||||
fmt.Fprint(c.OutputDest, ToTSV(values[1:]), "\n")
|
||||
}
|
||||
|
||||
// Println print as the values.
|
||||
func (c *Client) Println(values ...interface{}) {
|
||||
fmt.Fprint(c.OutputDest, ToTSV(values[1:]), "\n")
|
||||
}
|
||||
|
||||
// Get return *c as the get path of API request.
|
||||
func (c *Client) Get(v interface{}, path string) error {
|
||||
return c.APIReq(v, "GET", path, nil)
|
||||
}
|
||||
|
||||
// Patch return *c as the patch path of API request.
|
||||
func (c *Client) Patch(v interface{}, path string, body interface{}) error {
|
||||
return c.APIReq(v, "PATCH", path, body)
|
||||
}
|
||||
|
||||
// Post return *c as the post path of API request.
|
||||
func (c *Client) Post(v interface{}, path string, body interface{}) error {
|
||||
return c.APIReq(v, "POST", path, body)
|
||||
}
|
||||
|
||||
// Put return *c as the put path of API request.
|
||||
func (c *Client) Put(v interface{}, path string, body interface{}) error {
|
||||
return c.APIReq(v, "PUT", path, body)
|
||||
}
|
||||
|
||||
// Delete return *c as the delete path of API request.
|
||||
func (c *Client) Delete(path string) error {
|
||||
return c.APIReq(nil, "DELETE", path, nil)
|
||||
}
|
||||
|
||||
// NewClientWithOsExitOnErr return client.
|
||||
func NewClientWithOsExitOnErr() *Client {
|
||||
client, err := NewClient()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
// NewClient returns a new arukas client, requires an authorization key.
|
||||
// You can generate a API key by visiting the Keys section of the Arukas
|
||||
// control panel for your account.
|
||||
func NewClient() (*Client, error) {
|
||||
debug := false
|
||||
if os.Getenv("ARUKAS_DEBUG") != "" {
|
||||
debug = true
|
||||
}
|
||||
apiURL := "https://app.arukas.io/api/"
|
||||
if os.Getenv("ARUKAS_JSON_API_URL") != "" {
|
||||
apiURL = os.Getenv("ARUKAS_JSON_API_URL")
|
||||
}
|
||||
client := new(Client)
|
||||
parsedURL, err := url.Parse(apiURL)
|
||||
if err != nil {
|
||||
fmt.Println("url err")
|
||||
return nil, err
|
||||
}
|
||||
parsedURL.Path = strings.TrimRight(parsedURL.Path, "/")
|
||||
|
||||
client.APIURL = parsedURL
|
||||
client.UserAgent = "Arukas CLI (" + VERSION + ")"
|
||||
client.Debug = debug
|
||||
client.OutputDest = os.Stdout
|
||||
client.Timeout = 30 * time.Second
|
||||
|
||||
if username := os.Getenv("ARUKAS_JSON_API_TOKEN"); username != "" {
|
||||
client.Username = username
|
||||
} else {
|
||||
return nil, errors.New("ARUKAS_JSON_API_TOKEN is not set")
|
||||
}
|
||||
|
||||
if password := os.Getenv("ARUKAS_JSON_API_SECRET"); password != "" {
|
||||
client.Password = password
|
||||
} else {
|
||||
return nil, errors.New("ARUKAS_JSON_API_SECRET is not set")
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// NewRequest Generates an HTTP request for the Arukas API, but does not
|
||||
// perform the request. The request's Accept header field will be
|
||||
// set to:
|
||||
//
|
||||
// Accept: application/vnd.api+json;
|
||||
//
|
||||
// The type of body determines how to encode the request:
|
||||
//
|
||||
// nil no body
|
||||
// io.Reader body is sent verbatim
|
||||
// []byte body is encoded as application/vnd.api+json
|
||||
// else body is encoded as application/json
|
||||
func (c *Client) NewRequest(method, path string, body interface{}) (*http.Request, error) {
|
||||
var ctype string
|
||||
var rbody io.Reader
|
||||
|
||||
switch t := body.(type) {
|
||||
case nil:
|
||||
case string:
|
||||
rbody = bytes.NewBufferString(t)
|
||||
case io.Reader:
|
||||
rbody = t
|
||||
case []byte:
|
||||
rbody = bytes.NewReader(t)
|
||||
ctype = "application/vnd.api+json"
|
||||
default:
|
||||
v := reflect.ValueOf(body)
|
||||
if !v.IsValid() {
|
||||
break
|
||||
}
|
||||
if v.Type().Kind() == reflect.Ptr {
|
||||
v = reflect.Indirect(v)
|
||||
if !v.IsValid() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
j, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rbody = bytes.NewReader(j)
|
||||
ctype = "application/json"
|
||||
}
|
||||
requestURL := *c.APIURL // shallow copy
|
||||
requestURL.Path += path
|
||||
if c.Debug {
|
||||
fmt.Printf("Requesting: %s %s %s\n", method, requestURL, rbody)
|
||||
}
|
||||
req, err := http.NewRequest(method, requestURL.String(), rbody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/vnd.api+json")
|
||||
req.Header.Set("User-Agent", c.UserAgent)
|
||||
if ctype != "" {
|
||||
req.Header.Set("Content-Type", ctype)
|
||||
}
|
||||
req.SetBasicAuth(c.Username, c.Password)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// APIReq Sends a Arukas API request and decodes the response into v.
|
||||
// As described in NewRequest(), the type of body determines how to
|
||||
// encode the request body. As described in DoReq(), the type of
|
||||
// v determines how to handle the response body.
|
||||
func (c *Client) APIReq(v interface{}, method, path string, body interface{}) error {
|
||||
var marshaled []byte
|
||||
var err1 error
|
||||
var req *http.Request
|
||||
if body != nil {
|
||||
var err error
|
||||
_, ok := body.(jsonapi.MarshalIdentifier)
|
||||
if ok {
|
||||
marshaled, err = jsonapi.Marshal(body)
|
||||
} else {
|
||||
marshaled, err = json.Marshal(body)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Debug {
|
||||
fmt.Println("json: ", string(marshaled))
|
||||
}
|
||||
req, err1 = c.NewRequest(method, path, marshaled)
|
||||
} else {
|
||||
req, err1 = c.NewRequest(method, path, body)
|
||||
}
|
||||
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
return c.DoReq(req, v)
|
||||
}
|
||||
|
||||
// DoReq Submits an HTTP request, checks its response, and deserializes
|
||||
// the response into v. The type of v determines how to handle
|
||||
// the response body:
|
||||
//
|
||||
// nil body is discarded
|
||||
// io.Writer body is copied directly into v
|
||||
// else body is decoded into v as json
|
||||
//
|
||||
func (c *Client) DoReq(req *http.Request, v interface{}) error {
|
||||
|
||||
httpClient := c.HTTP
|
||||
if httpClient == nil {
|
||||
httpClient = &http.Client{
|
||||
Timeout: c.Timeout,
|
||||
}
|
||||
}
|
||||
|
||||
res, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
|
||||
if c.Debug {
|
||||
fmt.Println("Status:", res.StatusCode)
|
||||
headers := make([]string, len(res.Header))
|
||||
for k := range res.Header {
|
||||
headers = append(headers, k)
|
||||
}
|
||||
sort.Strings(headers)
|
||||
for _, k := range headers {
|
||||
if k != "" {
|
||||
fmt.Println(k+":", strings.Join(res.Header[k], " "))
|
||||
}
|
||||
}
|
||||
fmt.Println(string(body))
|
||||
}
|
||||
|
||||
if err = checkResponse(res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
case io.Writer:
|
||||
_, err = io.Copy(t, res.Body)
|
||||
default:
|
||||
err = jsonapi.Unmarshal(body, v)
|
||||
if err != nil {
|
||||
err = json.Unmarshal(body, v)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// CheckResponse returns an error (of type *Error) if the response.
|
||||
func checkResponse(res *http.Response) error {
|
||||
if res.StatusCode == 404 {
|
||||
return fmt.Errorf("The resource does not found on the server: %s", res.Request.URL)
|
||||
} else if res.StatusCode >= 400 {
|
||||
return fmt.Errorf("Got HTTP status code >= 400: %s", res.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrintTsvln print Tab-separated values line.
|
||||
func PrintTsvln(values ...interface{}) {
|
||||
fmt.Println(ToTSV(values))
|
||||
}
|
||||
|
||||
// ToTSV return Tab-separated values.
|
||||
func ToTSV(values []interface{}) string {
|
||||
var str []string
|
||||
for _, s := range values {
|
||||
if v, ok := s.(string); ok {
|
||||
str = append(str, string(v))
|
||||
} else {
|
||||
str = append(str, fmt.Sprint(s))
|
||||
}
|
||||
|
||||
}
|
||||
return strings.Join(str, "\t")
|
||||
}
|
||||
|
||||
// SplitTSV return splited Tab-separated values.
|
||||
func SplitTSV(str string) []string {
|
||||
splitStr := strings.Split(str, "\t")
|
||||
var trimmed []string
|
||||
for _, v := range splitStr {
|
||||
trimmed = append(trimmed, strings.Trim(v, "\n"))
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
// removeFirstLine is remove first line.
|
||||
func removeFirstLine(str string) string {
|
||||
lines := strings.Split(str, "\n")
|
||||
return strings.Join(lines[1:], "\n")
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
package arukas
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/manyminds/api2go/jsonapi"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PortMapping represents a docker container port mapping in struct variables.
|
||||
type PortMapping struct {
|
||||
ContainerPort int `json:"container_port"`
|
||||
ServicePort int `json:"service_port"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
// TaskPorts is Multiple PortMapping.
|
||||
type TaskPorts []PortMapping
|
||||
|
||||
// PortMappings is multiple TaskPorts.
|
||||
type PortMappings []TaskPorts
|
||||
|
||||
// Env represents a docker container environment key-value in struct variables.
|
||||
type Env struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// Envs is multiple Env.
|
||||
type Envs []Env
|
||||
|
||||
// Port represents a docker protocol and port-number in struct variables.
|
||||
type Port struct {
|
||||
Protocol string `json:"protocol"`
|
||||
Number int `json:"number"`
|
||||
}
|
||||
|
||||
// Ports is multiple Port.
|
||||
type Ports []Port
|
||||
|
||||
// Container represents a docker container data in struct variables.
|
||||
type Container struct {
|
||||
Envs Envs `json:"envs"`
|
||||
Ports Ports `json:"ports"`
|
||||
PortMappings PortMappings `json:"port_mappings,omitempty"`
|
||||
StatusText string `json:"status_text,omitempty"`
|
||||
ID string
|
||||
ImageName string `json:"image_name"`
|
||||
CreatedAt JSONTime `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
App *App
|
||||
Mem int `json:"mem"`
|
||||
AppID string `json:"app_id"`
|
||||
Instances int `json:"instances"`
|
||||
IsRunning bool `json:"is_running,omitempty"`
|
||||
Cmd string `json:"cmd"`
|
||||
Name string `json:"name"`
|
||||
Endpoint string `json:"end_point,omitempty"`
|
||||
}
|
||||
|
||||
// GetID returns a stringified of an ID.
|
||||
func (c Container) GetID() string {
|
||||
return string(c.ID)
|
||||
}
|
||||
|
||||
// SetID to satisfy jsonapi.UnmarshalIdentifier interface.
|
||||
func (c *Container) SetID(ID string) error {
|
||||
c.ID = ID
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetReferences returns all related structs to transactions.
|
||||
func (c Container) GetReferences() []jsonapi.Reference {
|
||||
return []jsonapi.Reference{
|
||||
{
|
||||
Type: "apps",
|
||||
Name: "app",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetReferencedIDs satisfies the jsonapi.MarshalLinkedRelations interface.
|
||||
func (c Container) GetReferencedIDs() []jsonapi.ReferenceID {
|
||||
result := []jsonapi.ReferenceID{}
|
||||
|
||||
if c.AppID != "" {
|
||||
result = append(result, jsonapi.ReferenceID{ID: c.AppID, Name: "app", Type: "apps"})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetReferencedStructs to satisfy the jsonapi.MarhsalIncludedRelations interface.
|
||||
func (c Container) GetReferencedStructs() []jsonapi.MarshalIdentifier {
|
||||
result := []jsonapi.MarshalIdentifier{}
|
||||
if c.App != nil {
|
||||
result = append(result, c.App)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SetToOneReferenceID sets the reference ID and satisfies the jsonapi.UnmarshalToOneRelations interface.
|
||||
func (c *Container) SetToOneReferenceID(name, ID string) error {
|
||||
if name == "app" {
|
||||
if ID == "" {
|
||||
c.App = nil
|
||||
} else {
|
||||
c.App = &App{ID: ID}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("There is no to-one relationship with the name " + name)
|
||||
}
|
||||
|
||||
// ParseEnv parse docker container envs.
|
||||
func ParseEnv(envs []string) (Envs, error) {
|
||||
var parsedEnvs Envs
|
||||
for _, env := range envs {
|
||||
kv := strings.Split(env, "=")
|
||||
parsedEnvs = append(parsedEnvs, Env{Key: kv[0], Value: kv[1]})
|
||||
}
|
||||
return parsedEnvs, nil
|
||||
}
|
||||
|
||||
// ParsePort parse docker container ports.
|
||||
func ParsePort(ports []string) (Ports, error) {
|
||||
var parsedPorts Ports
|
||||
for _, port := range ports {
|
||||
kv := strings.Split(port, ":")
|
||||
num, err := strconv.Atoi(kv[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Port number must be numeric. Given: %s", kv[0])
|
||||
}
|
||||
if !(kv[1] == "tcp" || kv[1] == "udp") {
|
||||
return nil, fmt.Errorf("Port protocol must be \"tcp\" or \"udp\"")
|
||||
}
|
||||
parsedPorts = append(parsedPorts, Port{Number: num, Protocol: kv[1]})
|
||||
}
|
||||
return parsedPorts, nil
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package arukas
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// JSONTime is time.Time that serializes as unix timestamp (in microseconds).
|
||||
type JSONTime time.Time
|
||||
|
||||
// UnmarshalJSON sets *t to a copy of data.
|
||||
func (t *JSONTime) UnmarshalJSON(data []byte) (err error) {
|
||||
parsed, err := time.Parse(`"`+time.RFC3339Nano+`"`, string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*t = JSONTime(parsed)
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalJSON returns t as the JSON encoding of t.
|
||||
func (t JSONTime) MarshalJSON() ([]byte, error) {
|
||||
stamp := fmt.Sprintf("\"%s\"", time.Time(t).Format(time.RFC3339Nano))
|
||||
return []byte(stamp), nil
|
||||
}
|
||||
|
||||
// String return t as the string of t.
|
||||
func (t JSONTime) String() string {
|
||||
return time.Time(t).Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
// Time return t as the time of t.
|
||||
func (t JSONTime) Time() time.Time {
|
||||
return time.Time(t)
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package arukas
|
||||
|
||||
import (
|
||||
// "errors"
|
||||
// "fmt"
|
||||
// "github.com/codegangsta/cli"
|
||||
// "os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// User represents a user data in struct variables.
|
||||
type User struct {
|
||||
ID string `json:"-"` // user id
|
||||
Name string `json:"name"` // user name
|
||||
Email string `json:"email"` // user e-mail
|
||||
Provider string `json:"provider"` // user oAuth provider
|
||||
ImageURL string `json:"image_url"` // user profile image
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ConfirmedAt time.Time `json:"-"`
|
||||
}
|
||||
|
||||
// GetID returns a stringified of an ID.
|
||||
func (u User) GetID() string {
|
||||
return string(u.ID)
|
||||
}
|
||||
|
||||
// SetID to satisfy jsonapi.UnmarshalIdentifier interface.
|
||||
func (u *User) SetID(ID string) error {
|
||||
u.ID = ID
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
The MIT License
|
||||
|
||||
CakePHP(tm) : The Rapid Development PHP Framework (http://cakephp.org)
|
||||
Copyright (c) 2005-2013, Cake Software Foundation, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Cake Software Foundation, Inc.
|
||||
1785 E. Sahara Avenue,
|
||||
Suite 490-204
|
||||
Las Vegas, Nevada 89104,
|
||||
United States of America.
|
|
@ -0,0 +1,29 @@
|
|||
Copyright (c) 2013 Akeda Bagus <admin@gedex.web.id>. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
----------
|
||||
|
||||
Much of this library was inspired from CakePHP's inflector, a PHP
|
||||
framework licensed under MIT license (see CakePHP_LICENSE.txt).
|
|
@ -0,0 +1,25 @@
|
|||
Inflector
|
||||
=========
|
||||
|
||||
Inflector pluralizes and singularizes English nouns.
|
||||
|
||||
[![Build Status](https://travis-ci.org/gedex/inflector.png?branch=master)](https://travis-ci.org/gedex/inflector)
|
||||
[![Coverage Status](https://coveralls.io/repos/gedex/inflector/badge.png?branch=master)](https://coveralls.io/r/gedex/inflector?branch=master)
|
||||
[![GoDoc](https://godoc.org/github.com/gedex/inflector?status.svg)](https://godoc.org/github.com/gedex/inflector)
|
||||
|
||||
## Basic Usage
|
||||
|
||||
There are only two exported functions: `Pluralize` and `Singularize`.
|
||||
|
||||
~~~go
|
||||
fmt.Println(inflector.Singularize("People")) // will print "Person"
|
||||
fmt.Println(inflector.Pluralize("octopus")) // will print "octopuses"
|
||||
~~~
|
||||
|
||||
## Credits
|
||||
|
||||
* [CakePHP's Inflector](https://github.com/cakephp/cakephp/blob/master/lib/Cake/Utility/Inflector.php)
|
||||
|
||||
## License
|
||||
|
||||
This library is distributed under the BSD-style license found in the LICENSE.md file.
|
|
@ -0,0 +1,355 @@
|
|||
// Copyright 2013 Akeda Bagus <admin@gedex.web.id>. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package inflector pluralizes and singularizes English nouns.
|
||||
|
||||
There are only two exported functions: `Pluralize` and `Singularize`.
|
||||
|
||||
s := "People"
|
||||
fmt.Println(inflector.Singularize(s)) // will print "Person"
|
||||
|
||||
s2 := "octopus"
|
||||
fmt.Println(inflector.Pluralize(s2)) // will print "octopuses"
|
||||
|
||||
*/
|
||||
package inflector
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Rule represents name of the inflector rule, can be
|
||||
// Plural or Singular
|
||||
type Rule int
|
||||
|
||||
const (
|
||||
Plural = iota
|
||||
Singular
|
||||
)
|
||||
|
||||
// InflectorRule represents inflector rule
|
||||
type InflectorRule struct {
|
||||
Rules []*ruleItem
|
||||
Irregular []*irregularItem
|
||||
Uninflected []string
|
||||
compiledIrregular *regexp.Regexp
|
||||
compiledUninflected *regexp.Regexp
|
||||
compiledRules []*compiledRule
|
||||
}
|
||||
|
||||
type ruleItem struct {
|
||||
pattern string
|
||||
replacement string
|
||||
}
|
||||
|
||||
type irregularItem struct {
|
||||
word string
|
||||
replacement string
|
||||
}
|
||||
|
||||
// compiledRule represents compiled version of Inflector.Rules.
|
||||
type compiledRule struct {
|
||||
replacement string
|
||||
*regexp.Regexp
|
||||
}
|
||||
|
||||
// threadsafe access to rules and caches
|
||||
var mutex sync.Mutex
|
||||
var rules = make(map[Rule]*InflectorRule)
|
||||
|
||||
// Words that should not be inflected
|
||||
var uninflected = []string{
|
||||
`Amoyese`, `bison`, `Borghese`, `bream`, `breeches`, `britches`, `buffalo`,
|
||||
`cantus`, `carp`, `chassis`, `clippers`, `cod`, `coitus`, `Congoese`,
|
||||
`contretemps`, `corps`, `debris`, `diabetes`, `djinn`, `eland`, `elk`,
|
||||
`equipment`, `Faroese`, `flounder`, `Foochowese`, `gallows`, `Genevese`,
|
||||
`Genoese`, `Gilbertese`, `graffiti`, `headquarters`, `herpes`, `hijinks`,
|
||||
`Hottentotese`, `information`, `innings`, `jackanapes`, `Kiplingese`,
|
||||
`Kongoese`, `Lucchese`, `mackerel`, `Maltese`, `.*?media`, `mews`, `moose`,
|
||||
`mumps`, `Nankingese`, `news`, `nexus`, `Niasese`, `Pekingese`,
|
||||
`Piedmontese`, `pincers`, `Pistoiese`, `pliers`, `Portuguese`, `proceedings`,
|
||||
`rabies`, `rice`, `rhinoceros`, `salmon`, `Sarawakese`, `scissors`,
|
||||
`sea[- ]bass`, `series`, `Shavese`, `shears`, `siemens`, `species`, `swine`,
|
||||
`testes`, `trousers`, `trout`, `tuna`, `Vermontese`, `Wenchowese`, `whiting`,
|
||||
`wildebeest`, `Yengeese`,
|
||||
}
|
||||
|
||||
// Plural words that should not be inflected
|
||||
var uninflectedPlurals = []string{
|
||||
`.*[nrlm]ese`, `.*deer`, `.*fish`, `.*measles`, `.*ois`, `.*pox`, `.*sheep`,
|
||||
`people`,
|
||||
}
|
||||
|
||||
// Singular words that should not be inflected
|
||||
var uninflectedSingulars = []string{
|
||||
`.*[nrlm]ese`, `.*deer`, `.*fish`, `.*measles`, `.*ois`, `.*pox`, `.*sheep`,
|
||||
`.*ss`,
|
||||
}
|
||||
|
||||
type cache map[string]string
|
||||
|
||||
// Inflected words that already cached for immediate retrieval from a given Rule
|
||||
var caches = make(map[Rule]cache)
|
||||
|
||||
// map of irregular words where its key is a word and its value is the replacement
|
||||
var irregularMaps = make(map[Rule]cache)
|
||||
|
||||
func init() {
|
||||
|
||||
rules[Plural] = &InflectorRule{
|
||||
Rules: []*ruleItem{
|
||||
{`(?i)(s)tatus$`, `${1}${2}tatuses`},
|
||||
{`(?i)(quiz)$`, `${1}zes`},
|
||||
{`(?i)^(ox)$`, `${1}${2}en`},
|
||||
{`(?i)([m|l])ouse$`, `${1}ice`},
|
||||
{`(?i)(matr|vert|ind)(ix|ex)$`, `${1}ices`},
|
||||
{`(?i)(x|ch|ss|sh)$`, `${1}es`},
|
||||
{`(?i)([^aeiouy]|qu)y$`, `${1}ies`},
|
||||
{`(?i)(hive)$`, `$1s`},
|
||||
{`(?i)(?:([^f])fe|([lre])f)$`, `${1}${2}ves`},
|
||||
{`(?i)sis$`, `ses`},
|
||||
{`(?i)([ti])um$`, `${1}a`},
|
||||
{`(?i)(p)erson$`, `${1}eople`},
|
||||
{`(?i)(m)an$`, `${1}en`},
|
||||
{`(?i)(c)hild$`, `${1}hildren`},
|
||||
{`(?i)(buffal|tomat)o$`, `${1}${2}oes`},
|
||||
{`(?i)(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$`, `${1}i`},
|
||||
{`(?i)us$`, `uses`},
|
||||
{`(?i)(alias)$`, `${1}es`},
|
||||
{`(?i)(ax|cris|test)is$`, `${1}es`},
|
||||
{`s$`, `s`},
|
||||
{`^$`, ``},
|
||||
{`$`, `s`},
|
||||
},
|
||||
Irregular: []*irregularItem{
|
||||
{`atlas`, `atlases`},
|
||||
{`beef`, `beefs`},
|
||||
{`brother`, `brothers`},
|
||||
{`cafe`, `cafes`},
|
||||
{`child`, `children`},
|
||||
{`cookie`, `cookies`},
|
||||
{`corpus`, `corpuses`},
|
||||
{`cow`, `cows`},
|
||||
{`ganglion`, `ganglions`},
|
||||
{`genie`, `genies`},
|
||||
{`genus`, `genera`},
|
||||
{`graffito`, `graffiti`},
|
||||
{`hoof`, `hoofs`},
|
||||
{`loaf`, `loaves`},
|
||||
{`man`, `men`},
|
||||
{`money`, `monies`},
|
||||
{`mongoose`, `mongooses`},
|
||||
{`move`, `moves`},
|
||||
{`mythos`, `mythoi`},
|
||||
{`niche`, `niches`},
|
||||
{`numen`, `numina`},
|
||||
{`occiput`, `occiputs`},
|
||||
{`octopus`, `octopuses`},
|
||||
{`opus`, `opuses`},
|
||||
{`ox`, `oxen`},
|
||||
{`penis`, `penises`},
|
||||
{`person`, `people`},
|
||||
{`sex`, `sexes`},
|
||||
{`soliloquy`, `soliloquies`},
|
||||
{`testis`, `testes`},
|
||||
{`trilby`, `trilbys`},
|
||||
{`turf`, `turfs`},
|
||||
{`potato`, `potatoes`},
|
||||
{`hero`, `heroes`},
|
||||
{`tooth`, `teeth`},
|
||||
{`goose`, `geese`},
|
||||
{`foot`, `feet`},
|
||||
},
|
||||
}
|
||||
prepare(Plural)
|
||||
|
||||
rules[Singular] = &InflectorRule{
|
||||
Rules: []*ruleItem{
|
||||
{`(?i)(s)tatuses$`, `${1}${2}tatus`},
|
||||
{`(?i)^(.*)(menu)s$`, `${1}${2}`},
|
||||
{`(?i)(quiz)zes$`, `$1`},
|
||||
{`(?i)(matr)ices$`, `${1}ix`},
|
||||
{`(?i)(vert|ind)ices$`, `${1}ex`},
|
||||
{`(?i)^(ox)en`, `$1`},
|
||||
{`(?i)(alias)(es)*$`, `$1`},
|
||||
{`(?i)(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$`, `${1}us`},
|
||||
{`(?i)([ftw]ax)es`, `$1`},
|
||||
{`(?i)(cris|ax|test)es$`, `${1}is`},
|
||||
{`(?i)(shoe|slave)s$`, `$1`},
|
||||
{`(?i)(o)es$`, `$1`},
|
||||
{`ouses$`, `ouse`},
|
||||
{`([^a])uses$`, `${1}us`},
|
||||
{`(?i)([m|l])ice$`, `${1}ouse`},
|
||||
{`(?i)(x|ch|ss|sh)es$`, `$1`},
|
||||
{`(?i)(m)ovies$`, `${1}${2}ovie`},
|
||||
{`(?i)(s)eries$`, `${1}${2}eries`},
|
||||
{`(?i)([^aeiouy]|qu)ies$`, `${1}y`},
|
||||
{`(?i)(tive)s$`, `$1`},
|
||||
{`(?i)([lre])ves$`, `${1}f`},
|
||||
{`(?i)([^fo])ves$`, `${1}fe`},
|
||||
{`(?i)(hive)s$`, `$1`},
|
||||
{`(?i)(drive)s$`, `$1`},
|
||||
{`(?i)(^analy)ses$`, `${1}sis`},
|
||||
{`(?i)(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$`, `${1}${2}sis`},
|
||||
{`(?i)([ti])a$`, `${1}um`},
|
||||
{`(?i)(p)eople$`, `${1}${2}erson`},
|
||||
{`(?i)(m)en$`, `${1}an`},
|
||||
{`(?i)(c)hildren$`, `${1}${2}hild`},
|
||||
{`(?i)(n)ews$`, `${1}${2}ews`},
|
||||
{`eaus$`, `eau`},
|
||||
{`^(.*us)$`, `$1`},
|
||||
{`(?i)s$`, ``},
|
||||
},
|
||||
Irregular: []*irregularItem{
|
||||
{`foes`, `foe`},
|
||||
{`waves`, `wave`},
|
||||
{`curves`, `curve`},
|
||||
{`atlases`, `atlas`},
|
||||
{`beefs`, `beef`},
|
||||
{`brothers`, `brother`},
|
||||
{`cafes`, `cafe`},
|
||||
{`children`, `child`},
|
||||
{`cookies`, `cookie`},
|
||||
{`corpuses`, `corpus`},
|
||||
{`cows`, `cow`},
|
||||
{`ganglions`, `ganglion`},
|
||||
{`genies`, `genie`},
|
||||
{`genera`, `genus`},
|
||||
{`graffiti`, `graffito`},
|
||||
{`hoofs`, `hoof`},
|
||||
{`loaves`, `loaf`},
|
||||
{`men`, `man`},
|
||||
{`monies`, `money`},
|
||||
{`mongooses`, `mongoose`},
|
||||
{`moves`, `move`},
|
||||
{`mythoi`, `mythos`},
|
||||
{`niches`, `niche`},
|
||||
{`numina`, `numen`},
|
||||
{`occiputs`, `occiput`},
|
||||
{`octopuses`, `octopus`},
|
||||
{`opuses`, `opus`},
|
||||
{`oxen`, `ox`},
|
||||
{`penises`, `penis`},
|
||||
{`people`, `person`},
|
||||
{`sexes`, `sex`},
|
||||
{`soliloquies`, `soliloquy`},
|
||||
{`testes`, `testis`},
|
||||
{`trilbys`, `trilby`},
|
||||
{`turfs`, `turf`},
|
||||
{`potatoes`, `potato`},
|
||||
{`heroes`, `hero`},
|
||||
{`teeth`, `tooth`},
|
||||
{`geese`, `goose`},
|
||||
{`feet`, `foot`},
|
||||
},
|
||||
}
|
||||
prepare(Singular)
|
||||
}
|
||||
|
||||
// prepare rule, e.g., compile the pattern.
|
||||
func prepare(r Rule) error {
|
||||
var reString string
|
||||
|
||||
switch r {
|
||||
case Plural:
|
||||
// Merge global uninflected with singularsUninflected
|
||||
rules[r].Uninflected = merge(uninflected, uninflectedPlurals)
|
||||
case Singular:
|
||||
// Merge global uninflected with singularsUninflected
|
||||
rules[r].Uninflected = merge(uninflected, uninflectedSingulars)
|
||||
}
|
||||
|
||||
// Set InflectorRule.compiledUninflected by joining InflectorRule.Uninflected into
|
||||
// a single string then compile it.
|
||||
reString = fmt.Sprintf(`(?i)(^(?:%s))$`, strings.Join(rules[r].Uninflected, `|`))
|
||||
rules[r].compiledUninflected = regexp.MustCompile(reString)
|
||||
|
||||
// Prepare irregularMaps
|
||||
irregularMaps[r] = make(cache, len(rules[r].Irregular))
|
||||
|
||||
// Set InflectorRule.compiledIrregular by joining the irregularItem.word of Inflector.Irregular
|
||||
// into a single string then compile it.
|
||||
vIrregulars := make([]string, len(rules[r].Irregular))
|
||||
for i, item := range rules[r].Irregular {
|
||||
vIrregulars[i] = item.word
|
||||
irregularMaps[r][item.word] = item.replacement
|
||||
}
|
||||
reString = fmt.Sprintf(`(?i)(.*)\b((?:%s))$`, strings.Join(vIrregulars, `|`))
|
||||
rules[r].compiledIrregular = regexp.MustCompile(reString)
|
||||
|
||||
// Compile all patterns in InflectorRule.Rules
|
||||
rules[r].compiledRules = make([]*compiledRule, len(rules[r].Rules))
|
||||
for i, item := range rules[r].Rules {
|
||||
rules[r].compiledRules[i] = &compiledRule{item.replacement, regexp.MustCompile(item.pattern)}
|
||||
}
|
||||
|
||||
// Prepare caches
|
||||
caches[r] = make(cache)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// merge slice a and slice b
|
||||
func merge(a []string, b []string) []string {
|
||||
result := make([]string, len(a)+len(b))
|
||||
copy(result, a)
|
||||
copy(result[len(a):], b)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Pluralize returns string s in plural form.
|
||||
func Pluralize(s string) string {
|
||||
return getInflected(Plural, s)
|
||||
}
|
||||
|
||||
// Singularize returns string s in singular form.
|
||||
func Singularize(s string) string {
|
||||
return getInflected(Singular, s)
|
||||
}
|
||||
|
||||
func getInflected(r Rule, s string) string {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
if v, ok := caches[r][s]; ok {
|
||||
return v
|
||||
}
|
||||
|
||||
// Check for irregular words
|
||||
if res := rules[r].compiledIrregular.FindStringSubmatch(s); len(res) >= 3 {
|
||||
var buf bytes.Buffer
|
||||
|
||||
buf.WriteString(res[1])
|
||||
buf.WriteString(s[0:1])
|
||||
buf.WriteString(irregularMaps[r][strings.ToLower(res[2])][1:])
|
||||
|
||||
// Cache it then returns
|
||||
caches[r][s] = buf.String()
|
||||
return caches[r][s]
|
||||
}
|
||||
|
||||
// Check for uninflected words
|
||||
if rules[r].compiledUninflected.MatchString(s) {
|
||||
caches[r][s] = s
|
||||
return caches[r][s]
|
||||
}
|
||||
|
||||
// Check each rule
|
||||
for _, re := range rules[r].compiledRules {
|
||||
if re.MatchString(s) {
|
||||
caches[r][s] = re.ReplaceAllString(s, re.replacement)
|
||||
return caches[r][s]
|
||||
}
|
||||
}
|
||||
|
||||
// Returns unaltered
|
||||
caches[r][s] = s
|
||||
return caches[r][s]
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 manyminds
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,14 @@
|
|||
# api2go JSONAPI package
|
||||
|
||||
This package contains [JSON API](http://jsonapi.org) compatible
|
||||
marshal und unmarshal functionality.
|
||||
|
||||
```
|
||||
go get github.com/manyminds/api2go/jsonapi
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
For information on how to use this package, please refer to the
|
||||
documentation on the [api2go](https://github.com/manyminds/api2go) main project,
|
||||
the integration_test.go or the [godoc](http://godoc.org/github.com/manyminds/api2go/jsonapi).
|
|
@ -0,0 +1,150 @@
|
|||
package jsonapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var objectSuffix = []byte("{")
|
||||
var arraySuffix = []byte("[")
|
||||
var stringSuffix = []byte(`"`)
|
||||
|
||||
// A Document represents a JSON API document as specified here: http://jsonapi.org.
|
||||
type Document struct {
|
||||
Links Links `json:"links,omitempty"`
|
||||
Data *DataContainer `json:"data"`
|
||||
Included []Data `json:"included,omitempty"`
|
||||
Meta map[string]interface{} `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// A DataContainer is used to marshal and unmarshal single objects and arrays
|
||||
// of objects.
|
||||
type DataContainer struct {
|
||||
DataObject *Data
|
||||
DataArray []Data
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals the JSON-encoded data to the DataObject field if the
|
||||
// root element is an object or to the DataArray field for arrays.
|
||||
func (c *DataContainer) UnmarshalJSON(payload []byte) error {
|
||||
if bytes.HasPrefix(payload, objectSuffix) {
|
||||
return json.Unmarshal(payload, &c.DataObject)
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(payload, arraySuffix) {
|
||||
return json.Unmarshal(payload, &c.DataArray)
|
||||
}
|
||||
|
||||
return errors.New("expected a JSON encoded object or array")
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of the DataArray field or the DataObject
|
||||
// field. It will return "null" if neither of them is set.
|
||||
func (c *DataContainer) MarshalJSON() ([]byte, error) {
|
||||
if c.DataArray != nil {
|
||||
return json.Marshal(c.DataArray)
|
||||
}
|
||||
|
||||
return json.Marshal(c.DataObject)
|
||||
}
|
||||
|
||||
// Link represents a link for return in the document.
|
||||
type Link struct {
|
||||
Href string `json:"href"`
|
||||
Meta map[string]interface{} `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON marshals a string value into the Href field or marshals an
|
||||
// object value into the whole struct.
|
||||
func (l *Link) UnmarshalJSON(payload []byte) error {
|
||||
if bytes.HasPrefix(payload, stringSuffix) {
|
||||
return json.Unmarshal(payload, &l.Href)
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(payload, objectSuffix) {
|
||||
obj := make(map[string]interface{})
|
||||
err := json.Unmarshal(payload, &obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var ok bool
|
||||
l.Href, ok = obj["href"].(string)
|
||||
if !ok {
|
||||
return errors.New(`link object expects a "href" key`)
|
||||
}
|
||||
l.Meta, _ = obj["meta"].(map[string]interface{})
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("expected a JSON encoded string or object")
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of only the Href field if the Meta
|
||||
// field is empty, otherwise it marshals the whole struct.
|
||||
func (l Link) MarshalJSON() ([]byte, error) {
|
||||
if len(l.Meta) == 0 {
|
||||
return json.Marshal(l.Href)
|
||||
}
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"href": l.Href,
|
||||
"meta": l.Meta,
|
||||
})
|
||||
}
|
||||
|
||||
// Links contains a map of custom Link objects as given by an element.
|
||||
type Links map[string]Link
|
||||
|
||||
// Data is a general struct for document data and included data.
|
||||
type Data struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Attributes json.RawMessage `json:"attributes"`
|
||||
Relationships map[string]Relationship `json:"relationships,omitempty"`
|
||||
Links Links `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
// Relationship contains reference IDs to the related structs
|
||||
type Relationship struct {
|
||||
Links Links `json:"links,omitempty"`
|
||||
Data *RelationshipDataContainer `json:"data,omitempty"`
|
||||
Meta map[string]interface{} `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// A RelationshipDataContainer is used to marshal and unmarshal single relationship
|
||||
// objects and arrays of relationship objects.
|
||||
type RelationshipDataContainer struct {
|
||||
DataObject *RelationshipData
|
||||
DataArray []RelationshipData
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals the JSON-encoded data to the DataObject field if the
|
||||
// root element is an object or to the DataArray field for arrays.
|
||||
func (c *RelationshipDataContainer) UnmarshalJSON(payload []byte) error {
|
||||
if bytes.HasPrefix(payload, objectSuffix) {
|
||||
// payload is an object
|
||||
return json.Unmarshal(payload, &c.DataObject)
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(payload, arraySuffix) {
|
||||
// payload is an array
|
||||
return json.Unmarshal(payload, &c.DataArray)
|
||||
}
|
||||
|
||||
return errors.New("Invalid json for relationship data array/object")
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of the DataArray field or the DataObject
|
||||
// field. It will return "null" if neither of them is set.
|
||||
func (c *RelationshipDataContainer) MarshalJSON() ([]byte, error) {
|
||||
if c.DataArray != nil {
|
||||
return json.Marshal(c.DataArray)
|
||||
}
|
||||
return json.Marshal(c.DataObject)
|
||||
}
|
||||
|
||||
// RelationshipData represents one specific reference ID.
|
||||
type RelationshipData struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package jsonapi
|
||||
|
||||
// The EntityNamer interface can be optionally implemented to directly return the
|
||||
// name of resource used for the "type" field.
|
||||
//
|
||||
// Note: By default the name is guessed from the struct name.
|
||||
type EntityNamer interface {
|
||||
GetName() string
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package jsonapi
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/gedex/inflector"
|
||||
)
|
||||
|
||||
// https://github.com/golang/lint/blob/3d26dc39376c307203d3a221bada26816b3073cf/lint.go#L482
|
||||
var commonInitialisms = map[string]bool{
|
||||
"API": true,
|
||||
"ASCII": true,
|
||||
"CPU": true,
|
||||
"CSS": true,
|
||||
"DNS": true,
|
||||
"EOF": true,
|
||||
"GUID": true,
|
||||
"HTML": true,
|
||||
"HTTP": true,
|
||||
"HTTPS": true,
|
||||
"ID": true,
|
||||
"IP": true,
|
||||
"JSON": true,
|
||||
"LHS": true,
|
||||
"QPS": true,
|
||||
"RAM": true,
|
||||
"RHS": true,
|
||||
"RPC": true,
|
||||
"SLA": true,
|
||||
"SMTP": true,
|
||||
"SSH": true,
|
||||
"TLS": true,
|
||||
"TTL": true,
|
||||
"UI": true,
|
||||
"UID": true,
|
||||
"UUID": true,
|
||||
"URI": true,
|
||||
"URL": true,
|
||||
"UTF8": true,
|
||||
"VM": true,
|
||||
"XML": true,
|
||||
"JWT": true,
|
||||
}
|
||||
|
||||
// Jsonify returns a JSON formatted key name from a go struct field name.
|
||||
func Jsonify(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if commonInitialisms[s] {
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
|
||||
rs := []rune(s)
|
||||
rs[0] = unicode.ToLower(rs[0])
|
||||
|
||||
return string(rs)
|
||||
}
|
||||
|
||||
// Pluralize returns the pluralization of a noun.
|
||||
func Pluralize(word string) string {
|
||||
return inflector.Pluralize(word)
|
||||
}
|
|
@ -0,0 +1,388 @@
|
|||
package jsonapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RelationshipType specifies the type of a relationship.
|
||||
type RelationshipType int
|
||||
|
||||
// The available relationship types.
|
||||
//
|
||||
// Note: DefaultRelationship guesses the relationship type based on the
|
||||
// pluralization of the reference name.
|
||||
const (
|
||||
DefaultRelationship RelationshipType = iota
|
||||
ToOneRelationship
|
||||
ToManyRelationship
|
||||
)
|
||||
|
||||
// The MarshalIdentifier interface is necessary to give an element a unique ID.
|
||||
//
|
||||
// Note: The implementation of this interface is mandatory.
|
||||
type MarshalIdentifier interface {
|
||||
GetID() string
|
||||
}
|
||||
|
||||
// ReferenceID contains all necessary information in order to reference another
|
||||
// struct in JSON API.
|
||||
type ReferenceID struct {
|
||||
ID string
|
||||
Type string
|
||||
Name string
|
||||
Relationship RelationshipType
|
||||
}
|
||||
|
||||
// A Reference information about possible references of a struct.
|
||||
//
|
||||
// Note: If IsNotLoaded is set to true, the `data` field will be omitted and only
|
||||
// the `links` object will be generated. You should do this if there are some
|
||||
// references, but you do not want to load them. Otherwise, if IsNotLoaded is
|
||||
// false and GetReferencedIDs() returns no IDs for this reference name, an
|
||||
// empty `data` field will be added which means that there are no references.
|
||||
type Reference struct {
|
||||
Type string
|
||||
Name string
|
||||
IsNotLoaded bool
|
||||
Relationship RelationshipType
|
||||
}
|
||||
|
||||
// The MarshalReferences interface must be implemented if the struct to be
|
||||
// serialized has relationships.
|
||||
type MarshalReferences interface {
|
||||
GetReferences() []Reference
|
||||
}
|
||||
|
||||
// The MarshalLinkedRelations interface must be implemented if there are
|
||||
// reference ids that should be included in the document.
|
||||
type MarshalLinkedRelations interface {
|
||||
MarshalReferences
|
||||
MarshalIdentifier
|
||||
GetReferencedIDs() []ReferenceID
|
||||
}
|
||||
|
||||
// The MarshalIncludedRelations interface must be implemented if referenced
|
||||
// structs should be included in the document.
|
||||
type MarshalIncludedRelations interface {
|
||||
MarshalReferences
|
||||
MarshalIdentifier
|
||||
GetReferencedStructs() []MarshalIdentifier
|
||||
}
|
||||
|
||||
// The MarshalCustomLinks interface can be implemented if the struct should
|
||||
// want any custom links.
|
||||
type MarshalCustomLinks interface {
|
||||
MarshalIdentifier
|
||||
GetCustomLinks(string) Links
|
||||
}
|
||||
|
||||
// A ServerInformation implementor can be passed to MarshalWithURLs to generate
|
||||
// the `self` and `related` urls inside `links`.
|
||||
type ServerInformation interface {
|
||||
GetBaseURL() string
|
||||
GetPrefix() string
|
||||
}
|
||||
|
||||
// MarshalWithURLs can be used to pass along a ServerInformation implementor.
|
||||
func MarshalWithURLs(data interface{}, information ServerInformation) ([]byte, error) {
|
||||
document, err := MarshalToStruct(data, information)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(document)
|
||||
}
|
||||
|
||||
// Marshal wraps data in a Document and returns its JSON encoding.
|
||||
//
|
||||
// Data can be a struct, a pointer to a struct or a slice of structs. All structs
|
||||
// must at least implement the `MarshalIdentifier` interface.
|
||||
func Marshal(data interface{}) ([]byte, error) {
|
||||
document, err := MarshalToStruct(data, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(document)
|
||||
}
|
||||
|
||||
// MarshalToStruct marshals an api2go compatible struct into a jsonapi Document
|
||||
// structure which then can be marshaled to JSON. You only need this method if
|
||||
// you want to extract or extend parts of the document. You should directly use
|
||||
// Marshal to get a []byte with JSON in it.
|
||||
func MarshalToStruct(data interface{}, information ServerInformation) (*Document, error) {
|
||||
if data == nil {
|
||||
return &Document{}, nil
|
||||
}
|
||||
|
||||
switch reflect.TypeOf(data).Kind() {
|
||||
case reflect.Slice:
|
||||
return marshalSlice(data, information)
|
||||
case reflect.Struct, reflect.Ptr:
|
||||
return marshalStruct(data.(MarshalIdentifier), information)
|
||||
default:
|
||||
return nil, errors.New("Marshal only accepts slice, struct or ptr types")
|
||||
}
|
||||
}
|
||||
|
||||
func marshalSlice(data interface{}, information ServerInformation) (*Document, error) {
|
||||
result := &Document{}
|
||||
|
||||
val := reflect.ValueOf(data)
|
||||
dataElements := make([]Data, val.Len())
|
||||
var referencedStructs []MarshalIdentifier
|
||||
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
k := val.Index(i).Interface()
|
||||
element, ok := k.(MarshalIdentifier)
|
||||
if !ok {
|
||||
return nil, errors.New("all elements within the slice must implement api2go.MarshalIdentifier")
|
||||
}
|
||||
|
||||
err := marshalData(element, &dataElements[i], information)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
included, ok := k.(MarshalIncludedRelations)
|
||||
if ok {
|
||||
referencedStructs = append(referencedStructs, included.GetReferencedStructs()...)
|
||||
}
|
||||
}
|
||||
|
||||
includedElements, err := filterDuplicates(referencedStructs, information)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result.Data = &DataContainer{
|
||||
DataArray: dataElements,
|
||||
}
|
||||
|
||||
if includedElements != nil && len(includedElements) > 0 {
|
||||
result.Included = includedElements
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func filterDuplicates(input []MarshalIdentifier, information ServerInformation) ([]Data, error) {
|
||||
alreadyIncluded := map[string]map[string]bool{}
|
||||
includedElements := []Data{}
|
||||
|
||||
for _, referencedStruct := range input {
|
||||
structType := getStructType(referencedStruct)
|
||||
|
||||
if alreadyIncluded[structType] == nil {
|
||||
alreadyIncluded[structType] = make(map[string]bool)
|
||||
}
|
||||
|
||||
if !alreadyIncluded[structType][referencedStruct.GetID()] {
|
||||
var data Data
|
||||
err := marshalData(referencedStruct, &data, information)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
includedElements = append(includedElements, data)
|
||||
alreadyIncluded[structType][referencedStruct.GetID()] = true
|
||||
}
|
||||
}
|
||||
|
||||
return includedElements, nil
|
||||
}
|
||||
|
||||
func marshalData(element MarshalIdentifier, data *Data, information ServerInformation) error {
|
||||
refValue := reflect.ValueOf(element)
|
||||
if refValue.Kind() == reflect.Ptr && refValue.IsNil() {
|
||||
return errors.New("MarshalIdentifier must not be nil")
|
||||
}
|
||||
|
||||
attributes, err := json.Marshal(element)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data.Attributes = attributes
|
||||
data.ID = element.GetID()
|
||||
data.Type = getStructType(element)
|
||||
|
||||
if information != nil {
|
||||
if customLinks, ok := element.(MarshalCustomLinks); ok {
|
||||
if data.Links == nil {
|
||||
data.Links = make(Links)
|
||||
}
|
||||
base := getLinkBaseURL(element, information)
|
||||
for k, v := range customLinks.GetCustomLinks(base) {
|
||||
if _, ok := data.Links[k]; !ok {
|
||||
data.Links[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if references, ok := element.(MarshalLinkedRelations); ok {
|
||||
data.Relationships = getStructRelationships(references, information)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isToMany(relationshipType RelationshipType, name string) bool {
|
||||
if relationshipType == DefaultRelationship {
|
||||
return Pluralize(name) == name
|
||||
}
|
||||
|
||||
return relationshipType == ToManyRelationship
|
||||
}
|
||||
|
||||
func getStructRelationships(relationer MarshalLinkedRelations, information ServerInformation) map[string]Relationship {
|
||||
referencedIDs := relationer.GetReferencedIDs()
|
||||
sortedResults := map[string][]ReferenceID{}
|
||||
relationships := map[string]Relationship{}
|
||||
|
||||
for _, referenceID := range referencedIDs {
|
||||
sortedResults[referenceID.Name] = append(sortedResults[referenceID.Name], referenceID)
|
||||
}
|
||||
|
||||
references := relationer.GetReferences()
|
||||
|
||||
// helper map to check if all references are included to also include empty ones
|
||||
notIncludedReferences := map[string]Reference{}
|
||||
for _, reference := range references {
|
||||
notIncludedReferences[reference.Name] = reference
|
||||
}
|
||||
|
||||
for name, referenceIDs := range sortedResults {
|
||||
relationships[name] = Relationship{}
|
||||
|
||||
// if referenceType is plural, we need to use an array for data, otherwise it's just an object
|
||||
container := RelationshipDataContainer{}
|
||||
|
||||
if isToMany(referenceIDs[0].Relationship, referenceIDs[0].Name) {
|
||||
// multiple elements in links
|
||||
container.DataArray = []RelationshipData{}
|
||||
for _, referenceID := range referenceIDs {
|
||||
container.DataArray = append(container.DataArray, RelationshipData{
|
||||
Type: referenceID.Type,
|
||||
ID: referenceID.ID,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
container.DataObject = &RelationshipData{
|
||||
Type: referenceIDs[0].Type,
|
||||
ID: referenceIDs[0].ID,
|
||||
}
|
||||
}
|
||||
|
||||
// set URLs if necessary
|
||||
links := getLinksForServerInformation(relationer, name, information)
|
||||
|
||||
relationship := Relationship{
|
||||
Data: &container,
|
||||
Links: links,
|
||||
}
|
||||
|
||||
relationships[name] = relationship
|
||||
|
||||
// this marks the reference as already included
|
||||
delete(notIncludedReferences, referenceIDs[0].Name)
|
||||
}
|
||||
|
||||
// check for empty references
|
||||
for name, reference := range notIncludedReferences {
|
||||
container := RelationshipDataContainer{}
|
||||
|
||||
// Plural empty relationships need an empty array and empty to-one need a null in the json
|
||||
if !reference.IsNotLoaded && isToMany(reference.Relationship, reference.Name) {
|
||||
container.DataArray = []RelationshipData{}
|
||||
}
|
||||
|
||||
links := getLinksForServerInformation(relationer, name, information)
|
||||
relationship := Relationship{
|
||||
Links: links,
|
||||
}
|
||||
|
||||
// skip relationship data completely if IsNotLoaded is set
|
||||
if !reference.IsNotLoaded {
|
||||
relationship.Data = &container
|
||||
}
|
||||
|
||||
relationships[name] = relationship
|
||||
}
|
||||
|
||||
return relationships
|
||||
}
|
||||
|
||||
func getLinkBaseURL(element MarshalIdentifier, information ServerInformation) string {
|
||||
prefix := strings.Trim(information.GetBaseURL(), "/")
|
||||
namespace := strings.Trim(information.GetPrefix(), "/")
|
||||
structType := getStructType(element)
|
||||
|
||||
if namespace != "" {
|
||||
prefix += "/" + namespace
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/%s/%s", prefix, structType, element.GetID())
|
||||
}
|
||||
|
||||
func getLinksForServerInformation(relationer MarshalLinkedRelations, name string, information ServerInformation) Links {
|
||||
if information == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
links := make(Links)
|
||||
base := getLinkBaseURL(relationer, information)
|
||||
|
||||
links["self"] = Link{Href: fmt.Sprintf("%s/relationships/%s", base, name)}
|
||||
links["related"] = Link{Href: fmt.Sprintf("%s/%s", base, name)}
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
func marshalStruct(data MarshalIdentifier, information ServerInformation) (*Document, error) {
|
||||
var contentData Data
|
||||
|
||||
err := marshalData(data, &contentData, information)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &Document{
|
||||
Data: &DataContainer{
|
||||
DataObject: &contentData,
|
||||
},
|
||||
}
|
||||
|
||||
included, ok := data.(MarshalIncludedRelations)
|
||||
if ok {
|
||||
included, err := filterDuplicates(included.GetReferencedStructs(), information)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(included) > 0 {
|
||||
result.Included = included
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getStructType(data interface{}) string {
|
||||
entityName, ok := data.(EntityNamer)
|
||||
if ok {
|
||||
return entityName.GetName()
|
||||
}
|
||||
|
||||
reflectType := reflect.TypeOf(data)
|
||||
if reflectType.Kind() == reflect.Ptr {
|
||||
return Pluralize(Jsonify(reflectType.Elem().Name()))
|
||||
}
|
||||
|
||||
return Pluralize(Jsonify(reflectType.Name()))
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
package jsonapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// The UnmarshalIdentifier interface must be implemented to set the ID during
|
||||
// unmarshalling.
|
||||
type UnmarshalIdentifier interface {
|
||||
SetID(string) error
|
||||
}
|
||||
|
||||
// The UnmarshalToOneRelations interface must be implemented to unmarshal
|
||||
// to-one relations.
|
||||
type UnmarshalToOneRelations interface {
|
||||
SetToOneReferenceID(name, ID string) error
|
||||
}
|
||||
|
||||
// The UnmarshalToManyRelations interface must be implemented to unmarshal
|
||||
// to-many relations.
|
||||
type UnmarshalToManyRelations interface {
|
||||
SetToManyReferenceIDs(name string, IDs []string) error
|
||||
}
|
||||
|
||||
// The EditToManyRelations interface can be optionally implemented to add and
|
||||
// delete to-many relationships on a already unmarshalled struct. These methods
|
||||
// are used by our API for the to-many relationship update routes.
|
||||
//
|
||||
// There are 3 HTTP Methods to edit to-many relations:
|
||||
//
|
||||
// PATCH /v1/posts/1/comments
|
||||
// Content-Type: application/vnd.api+json
|
||||
// Accept: application/vnd.api+json
|
||||
//
|
||||
// {
|
||||
// "data": [
|
||||
// { "type": "comments", "id": "2" },
|
||||
// { "type": "comments", "id": "3" }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// This replaces all of the comments that belong to post with ID 1 and the
|
||||
// SetToManyReferenceIDs method will be called.
|
||||
//
|
||||
// POST /v1/posts/1/comments
|
||||
// Content-Type: application/vnd.api+json
|
||||
// Accept: application/vnd.api+json
|
||||
//
|
||||
// {
|
||||
// "data": [
|
||||
// { "type": "comments", "id": "123" }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// Adds a new comment to the post with ID 1.
|
||||
// The AddToManyIDs method will be called.
|
||||
//
|
||||
// DELETE /v1/posts/1/comments
|
||||
// Content-Type: application/vnd.api+json
|
||||
// Accept: application/vnd.api+json
|
||||
//
|
||||
// {
|
||||
// "data": [
|
||||
// { "type": "comments", "id": "12" },
|
||||
// { "type": "comments", "id": "13" }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// Deletes comments that belong to post with ID 1.
|
||||
// The DeleteToManyIDs method will be called.
|
||||
type EditToManyRelations interface {
|
||||
AddToManyIDs(name string, IDs []string) error
|
||||
DeleteToManyIDs(name string, IDs []string) error
|
||||
}
|
||||
|
||||
// Unmarshal parses a JSON API compatible JSON and populates the target which
|
||||
// must implement the `UnmarshalIdentifier` interface.
|
||||
func Unmarshal(data []byte, target interface{}) error {
|
||||
if target == nil {
|
||||
return errors.New("target must not be nil")
|
||||
}
|
||||
|
||||
if reflect.TypeOf(target).Kind() != reflect.Ptr {
|
||||
return errors.New("target must be a ptr")
|
||||
}
|
||||
|
||||
ctx := &Document{}
|
||||
|
||||
err := json.Unmarshal(data, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Data == nil {
|
||||
return errors.New(`Source JSON is empty and has no "attributes" payload object`)
|
||||
}
|
||||
|
||||
if ctx.Data.DataObject != nil {
|
||||
return setDataIntoTarget(ctx.Data.DataObject, target)
|
||||
}
|
||||
|
||||
if ctx.Data.DataArray != nil {
|
||||
targetSlice := reflect.TypeOf(target).Elem()
|
||||
if targetSlice.Kind() != reflect.Slice {
|
||||
return fmt.Errorf("Cannot unmarshal array to struct target %s", targetSlice)
|
||||
}
|
||||
targetType := targetSlice.Elem()
|
||||
targetPointer := reflect.ValueOf(target)
|
||||
targetValue := targetPointer.Elem()
|
||||
|
||||
for _, record := range ctx.Data.DataArray {
|
||||
// check if there already is an entry with the same id in target slice,
|
||||
// otherwise create a new target and append
|
||||
var targetRecord, emptyValue reflect.Value
|
||||
for i := 0; i < targetValue.Len(); i++ {
|
||||
marshalCasted, ok := targetValue.Index(i).Interface().(MarshalIdentifier)
|
||||
if !ok {
|
||||
return errors.New("existing structs must implement interface MarshalIdentifier")
|
||||
}
|
||||
if record.ID == marshalCasted.GetID() {
|
||||
targetRecord = targetValue.Index(i).Addr()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetRecord == emptyValue || targetRecord.IsNil() {
|
||||
targetRecord = reflect.New(targetType)
|
||||
err := setDataIntoTarget(&record, targetRecord.Interface())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetValue = reflect.Append(targetValue, targetRecord.Elem())
|
||||
} else {
|
||||
err := setDataIntoTarget(&record, targetRecord.Interface())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
targetPointer.Elem().Set(targetValue)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setDataIntoTarget(data *Data, target interface{}) error {
|
||||
castedTarget, ok := target.(UnmarshalIdentifier)
|
||||
if !ok {
|
||||
return errors.New("target must implement UnmarshalIdentifier interface")
|
||||
}
|
||||
|
||||
if data.Type == "" {
|
||||
return errors.New("invalid record, no type was specified")
|
||||
}
|
||||
|
||||
err := checkType(data.Type, castedTarget)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if data.Attributes != nil {
|
||||
err = json.Unmarshal(data.Attributes, castedTarget)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := castedTarget.SetID(data.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return setRelationshipIDs(data.Relationships, castedTarget)
|
||||
}
|
||||
|
||||
// extracts all found relationships and set's them via SetToOneReferenceID or
|
||||
// SetToManyReferenceIDs
|
||||
func setRelationshipIDs(relationships map[string]Relationship, target UnmarshalIdentifier) error {
|
||||
for name, rel := range relationships {
|
||||
// if Data is nil, it means that we have an empty toOne relationship
|
||||
if rel.Data == nil {
|
||||
castedToOne, ok := target.(UnmarshalToOneRelations)
|
||||
if !ok {
|
||||
return fmt.Errorf("struct %s does not implement UnmarshalToOneRelations", reflect.TypeOf(target))
|
||||
}
|
||||
|
||||
castedToOne.SetToOneReferenceID(name, "")
|
||||
break
|
||||
}
|
||||
|
||||
// valid toOne case
|
||||
if rel.Data.DataObject != nil {
|
||||
castedToOne, ok := target.(UnmarshalToOneRelations)
|
||||
if !ok {
|
||||
return fmt.Errorf("struct %s does not implement UnmarshalToOneRelations", reflect.TypeOf(target))
|
||||
}
|
||||
err := castedToOne.SetToOneReferenceID(name, rel.Data.DataObject.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// valid toMany case
|
||||
if rel.Data.DataArray != nil {
|
||||
castedToMany, ok := target.(UnmarshalToManyRelations)
|
||||
if !ok {
|
||||
return fmt.Errorf("struct %s does not implement UnmarshalToManyRelations", reflect.TypeOf(target))
|
||||
}
|
||||
IDs := make([]string, len(rel.Data.DataArray))
|
||||
for index, relData := range rel.Data.DataArray {
|
||||
IDs[index] = relData.ID
|
||||
}
|
||||
err := castedToMany.SetToManyReferenceIDs(name, IDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkType(incomingType string, target UnmarshalIdentifier) error {
|
||||
actualType := getStructType(target)
|
||||
if incomingType != actualType {
|
||||
return fmt.Errorf("Type %s in JSON does not match target struct type %s", incomingType, actualType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -461,6 +461,12 @@
|
|||
"revision": "4239b77079c7b5d1243b7b4736304ce8ddb6f0f2",
|
||||
"revisionTime": "2016-01-15T23:47:25Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "p9hyEP07p5jnjgUKHjKftWaKgs8=",
|
||||
"path": "github.com/arukasio/cli",
|
||||
"revision": "4f0dee167044ef44260d985a4d325f35a06e3149",
|
||||
"revisionTime": "2017-01-23T00:46:44Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "FWMPfJtpwiMBj9Ni0eMCVQYPV+M=",
|
||||
"path": "github.com/aws/aws-sdk-go",
|
||||
|
@ -1265,6 +1271,12 @@
|
|||
"revisionTime": "2016-04-27T17:25:47Z",
|
||||
"tree": true
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "SYxDN6l6j6OJ9f9BQDzneaJBd0k=",
|
||||
"path": "github.com/gedex/inflector",
|
||||
"revision": "046f2c31204676e3623730ee70fd0964e05822e7",
|
||||
"revisionTime": "2016-11-03T04:27:56Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "JKjnR1ApU6NcC79xcGaT7QRMx3A=",
|
||||
"comment": "0.0.1-42-gea19666",
|
||||
|
@ -2057,6 +2069,12 @@
|
|||
"revision": "d392059301313eee8059c85a4e698f22c664ef78",
|
||||
"revisionTime": "2015-10-10T02:14:09Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "BMxNUz0/EaTvexw9Vxm5jPG3w60=",
|
||||
"path": "github.com/manyminds/api2go/jsonapi",
|
||||
"revision": "dc368bb579c1a582faed50768e7fbcc463954c89",
|
||||
"revisionTime": "2016-12-29T20:22:12Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "yg57Q4J8Ob0LoYvqDxsWZ6AHffE=",
|
||||
"path": "github.com/masterzen/simplexml/dom",
|
||||
|
|
|
@ -9,6 +9,7 @@ body.page-sub{
|
|||
body.layout-commands-state,
|
||||
body.layout-alicloud,
|
||||
body.layout-archive,
|
||||
body.layout-arukas,
|
||||
body.layout-atlas,
|
||||
body.layout-aws,
|
||||
body.layout-azure,
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
layout: "arukas"
|
||||
page_title: "Provider: Arukas"
|
||||
sidebar_current: "docs-arukas-index"
|
||||
description: |-
|
||||
The Arukas provider is used to interact with the resources supported by Arukas.
|
||||
---
|
||||
|
||||
# Arukas Provider
|
||||
|
||||
The Arukas provider is used to manage [Arukas](https://arukas.io/en/) resources.
|
||||
|
||||
Use the navigation to the left to read about the available resources.
|
||||
|
||||
For additional details please refer to [Arukas documentation](https://arukas.io/en/category/documents-en/).
|
||||
|
||||
## Example Usage
|
||||
|
||||
Here is an example that will setup the following:
|
||||
|
||||
+ A container resource using the "NGINX" image
|
||||
+ Instance count is 1
|
||||
+ Memory size is 256Mbyte
|
||||
+ Expose tcp 80 port to the EndPoint
|
||||
+ Set environments variable with like "key1=value1"
|
||||
|
||||
Add the below to a file called `arukas.tf` and run the `terraform` command from the same directory:
|
||||
|
||||
```hcl
|
||||
provider "arukas" {
|
||||
token = ""
|
||||
secret = ""
|
||||
}
|
||||
|
||||
resource "arukas_container" "foobar" {
|
||||
name = "terraform_for_arukas_test_foobar"
|
||||
image = "nginx:latest"
|
||||
instances = 1
|
||||
memory = 256
|
||||
ports = {
|
||||
protocol = "tcp"
|
||||
number = "80"
|
||||
}
|
||||
environments {
|
||||
key = "key1"
|
||||
value = "value1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You'll need to provide your Arukas API token and secret,
|
||||
so that Terraform can connect. If you don't want to put
|
||||
credentials in your configuration file, you can leave them
|
||||
out:
|
||||
|
||||
```
|
||||
provider "arukas" {}
|
||||
```
|
||||
|
||||
...and instead set these environment variables:
|
||||
|
||||
- `ARUKAS_JSON_API_TOKEN` : Your Arukas API token
|
||||
- `ARUKAS_JSON_API_SECRET`: Your Arukas API secret
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
* `token` - (Required) This is the Arukas API token. It must be provided, but
|
||||
it can also be sourced from the `ARUKAS_JSON_API_TOKEN` environment variable.
|
||||
|
||||
* `secret` - (Required) This is the Arukas API secret. It must be provided, but
|
||||
it can also be sourced from the `ARUKAS_JSON_API_SECRET` environment variable.
|
||||
|
||||
* `api_url` - (Optional) Override Arukas API Root URL. Also taken from the `ARUKAS_JSON_API_URL`
|
||||
environment variable if provided.
|
||||
|
||||
* `trace` - (Optional) The flag of Arukas API trace log. Also taken from the `ARUKAS_DEBUG`
|
||||
environment variable if provided.
|
||||
|
||||
* `timeout` - (Optional) Override Arukas API timeout seconds. Also taken from the `ARUKAS_TIMEOUT`
|
||||
environment variable if provided.
|
|
@ -0,0 +1,98 @@
|
|||
---
|
||||
layout: "arukas"
|
||||
page_title: "Arukas: container"
|
||||
sidebar_current: "docs-arukas-resource-container"
|
||||
description: |-
|
||||
Manages Arukas Containers
|
||||
---
|
||||
|
||||
# arukas container
|
||||
|
||||
Provides container resource. This allows container to be created, updated and deleted.
|
||||
|
||||
For additional details please refer to [API documentation](https://arukas.io/en/documents-en/arukas-api-reference-en/#containers).
|
||||
|
||||
## Example Usage
|
||||
|
||||
Create a new container using the "NGINX" image.
|
||||
|
||||
```hcl
|
||||
resource "arukas_container" "foobar" {
|
||||
name = "terraform_for_arukas_test_foobar"
|
||||
image = "nginx:latest"
|
||||
instances = 1
|
||||
memory = 256
|
||||
ports = {
|
||||
protocol = "tcp"
|
||||
number = "80"
|
||||
}
|
||||
environments {
|
||||
key = "key1"
|
||||
value = "value1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
* `name` - (Required, string) The name of the container.
|
||||
* `image` - (Required, string) The ID of the image to back this container.It must be a public image on DockerHub.
|
||||
* `instances` - (Optional, int) The count of the instance. It must be between `1` and `10`.
|
||||
* `memory` - (Optional, int) The size of the instance RAM.It must be `256` or `512`.
|
||||
* `endpoint` - (Optional,string) The subdomain part of the endpoint assigned by Arukas. If it is not set, Arukas will do automatic assignment.
|
||||
* `ports` - (Required , block) See [Ports](#ports) below for details.
|
||||
* `environments` - (Required , block) See [Environments](#environments) below for details.
|
||||
* `cmd` - (Optional , string) The command of the container.
|
||||
|
||||
<a id="ports"></a>
|
||||
### Ports
|
||||
|
||||
`ports` is a block within the configuration that can be repeated to specify
|
||||
the port mappings of the container. Each `ports` block supports
|
||||
the following:
|
||||
|
||||
* `protocol` - (Optional, string) Protocol that can be used over this port, defaults to `tcp`,It must be `tcp` or `udp`.
|
||||
* `number` - (Optional, int) Port within the container,defaults to `80`, It must be between `1` to `65535`.
|
||||
|
||||
<a id="environments"></a>
|
||||
### Environments
|
||||
|
||||
`environments` is a block within the configuration that can be repeated to specify
|
||||
the environment variables. Each `environments` block supports
|
||||
the following:
|
||||
|
||||
* `key` - (Required, string) Key of environment variable.
|
||||
* `value` - (Required, string) Value of environment variable.
|
||||
|
||||
|
||||
## Attributes Reference
|
||||
|
||||
The following attributes are exported:
|
||||
|
||||
* `id` - The ID of the container.
|
||||
* `app_id` - The ID of the Arukas application to which the container belongs.
|
||||
* `name` - The name of the container.
|
||||
* `image` - The ID of the image to back this container.
|
||||
* `instances` - The count of the instance.
|
||||
* `memory` - The size of the instance RAM.
|
||||
* `endpoint` - The subdomain part of the endpoint assigned by Arukas.
|
||||
* `ports` - See [Ports](#ports) below for details.
|
||||
* `environments` - See [Environments](#environments) below for details.
|
||||
* `cmd` - The command of the container.
|
||||
* `port_mappings` - See [PortMappings](#port_mappings) below for details.
|
||||
* `endpoint_full_url` - The URL of endpoint.
|
||||
* `endpoint_full_hostname` - The Hostname of endpoint.
|
||||
|
||||
<a id="port_mappings"></a>
|
||||
### PortMappings
|
||||
|
||||
`port_mappings` is a block within the configuration that
|
||||
the port mappings of the container. Each `port_mappings` block supports
|
||||
the following:
|
||||
|
||||
* `host` - The name of the host actually running the container.
|
||||
* `ipaddress` - The IP address of the host actually running the container.
|
||||
* `container_port` - Port within the container.
|
||||
* `service_port` - The actual port mapped to the port in the container.
|
|
@ -0,0 +1,26 @@
|
|||
<% wrap_layout :inner do %>
|
||||
<% content_for :sidebar do %>
|
||||
<div class="docs-sidebar hidden-print affix-top" role="complementary">
|
||||
<ul class="nav docs-sidenav">
|
||||
<li<%= sidebar_current("docs-home") %>>
|
||||
<a href="/docs/providers/index.html">« Documentation Home</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-arukas-index") %>>
|
||||
<a href="/docs/providers/arukas/index.html">Arukas Provider</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current(/^docs-arukas-resource/) %>>
|
||||
<a href="#">Resources</a>
|
||||
<ul class="nav nav-visible">
|
||||
<li<%= sidebar_current("docs-arukas-resource-container") %>>
|
||||
<a href="/docs/providers/arukas/r/container.html">arukas_container</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= yield %>
|
||||
<% end %>
|
|
@ -182,6 +182,10 @@
|
|||
<a href="/docs/providers/archive/index.html">Archive</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-providers-arukas") %>>
|
||||
<a href="/docs/providers/arukas/index.html">Arukas</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-providers-atlas") %>>
|
||||
<a href="/docs/providers/atlas/index.html">Atlas</a>
|
||||
</li>
|
||||
|
|
Loading…
Reference in New Issue