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 (
|
import (
|
||||||
alicloudprovider "github.com/hashicorp/terraform/builtin/providers/alicloud"
|
alicloudprovider "github.com/hashicorp/terraform/builtin/providers/alicloud"
|
||||||
archiveprovider "github.com/hashicorp/terraform/builtin/providers/archive"
|
archiveprovider "github.com/hashicorp/terraform/builtin/providers/archive"
|
||||||
|
arukasprovider "github.com/hashicorp/terraform/builtin/providers/arukas"
|
||||||
atlasprovider "github.com/hashicorp/terraform/builtin/providers/atlas"
|
atlasprovider "github.com/hashicorp/terraform/builtin/providers/atlas"
|
||||||
awsprovider "github.com/hashicorp/terraform/builtin/providers/aws"
|
awsprovider "github.com/hashicorp/terraform/builtin/providers/aws"
|
||||||
azureprovider "github.com/hashicorp/terraform/builtin/providers/azure"
|
azureprovider "github.com/hashicorp/terraform/builtin/providers/azure"
|
||||||
|
@ -79,6 +80,7 @@ import (
|
||||||
var InternalProviders = map[string]plugin.ProviderFunc{
|
var InternalProviders = map[string]plugin.ProviderFunc{
|
||||||
"alicloud": alicloudprovider.Provider,
|
"alicloud": alicloudprovider.Provider,
|
||||||
"archive": archiveprovider.Provider,
|
"archive": archiveprovider.Provider,
|
||||||
|
"arukas": arukasprovider.Provider,
|
||||||
"atlas": atlasprovider.Provider,
|
"atlas": atlasprovider.Provider,
|
||||||
"aws": awsprovider.Provider,
|
"aws": awsprovider.Provider,
|
||||||
"azure": azureprovider.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",
|
"revision": "4239b77079c7b5d1243b7b4736304ce8ddb6f0f2",
|
||||||
"revisionTime": "2016-01-15T23:47:25Z"
|
"revisionTime": "2016-01-15T23:47:25Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "p9hyEP07p5jnjgUKHjKftWaKgs8=",
|
||||||
|
"path": "github.com/arukasio/cli",
|
||||||
|
"revision": "4f0dee167044ef44260d985a4d325f35a06e3149",
|
||||||
|
"revisionTime": "2017-01-23T00:46:44Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "FWMPfJtpwiMBj9Ni0eMCVQYPV+M=",
|
"checksumSHA1": "FWMPfJtpwiMBj9Ni0eMCVQYPV+M=",
|
||||||
"path": "github.com/aws/aws-sdk-go",
|
"path": "github.com/aws/aws-sdk-go",
|
||||||
|
@ -1265,6 +1271,12 @@
|
||||||
"revisionTime": "2016-04-27T17:25:47Z",
|
"revisionTime": "2016-04-27T17:25:47Z",
|
||||||
"tree": true
|
"tree": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "SYxDN6l6j6OJ9f9BQDzneaJBd0k=",
|
||||||
|
"path": "github.com/gedex/inflector",
|
||||||
|
"revision": "046f2c31204676e3623730ee70fd0964e05822e7",
|
||||||
|
"revisionTime": "2016-11-03T04:27:56Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "JKjnR1ApU6NcC79xcGaT7QRMx3A=",
|
"checksumSHA1": "JKjnR1ApU6NcC79xcGaT7QRMx3A=",
|
||||||
"comment": "0.0.1-42-gea19666",
|
"comment": "0.0.1-42-gea19666",
|
||||||
|
@ -2057,6 +2069,12 @@
|
||||||
"revision": "d392059301313eee8059c85a4e698f22c664ef78",
|
"revision": "d392059301313eee8059c85a4e698f22c664ef78",
|
||||||
"revisionTime": "2015-10-10T02:14:09Z"
|
"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=",
|
"checksumSHA1": "yg57Q4J8Ob0LoYvqDxsWZ6AHffE=",
|
||||||
"path": "github.com/masterzen/simplexml/dom",
|
"path": "github.com/masterzen/simplexml/dom",
|
||||||
|
|
|
@ -9,6 +9,7 @@ body.page-sub{
|
||||||
body.layout-commands-state,
|
body.layout-commands-state,
|
||||||
body.layout-alicloud,
|
body.layout-alicloud,
|
||||||
body.layout-archive,
|
body.layout-archive,
|
||||||
|
body.layout-arukas,
|
||||||
body.layout-atlas,
|
body.layout-atlas,
|
||||||
body.layout-aws,
|
body.layout-aws,
|
||||||
body.layout-azure,
|
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>
|
<a href="/docs/providers/archive/index.html">Archive</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-providers-arukas") %>>
|
||||||
|
<a href="/docs/providers/arukas/index.html">Arukas</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-providers-atlas") %>>
|
<li<%= sidebar_current("docs-providers-atlas") %>>
|
||||||
<a href="/docs/providers/atlas/index.html">Atlas</a>
|
<a href="/docs/providers/atlas/index.html">Atlas</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
Loading…
Reference in New Issue