Repository: appc/spec
Pull request #666 (2016-10-14 12:40:12)

*: merge various extensions to master


Patch:
diff --git a/actool/manifest.go b/actool/manifest.go
index feae114..98b9eaf 100644
--- a/actool/manifest.go
+++ b/actool/manifest.go
@@ -279,7 +279,7 @@ func patchManifest(im *schema.ImageManifest) error {
 			_, ok := types.ResourceIsolatorNames[name]
 
 			switch name {
-			case types.LinuxNoNewPrivilegesName:
+			case types.LinuxNoNewPrivilegesName, types.LinuxOOMScoreAdjName:
 				ok = true
 				kv := strings.Split(is, ",")
 				if len(kv) != 2 {
diff --git a/discovery/http.go b/discovery/http.go
index 7f30e66..66a144b 100644
--- a/discovery/http.go
+++ b/discovery/http.go
@@ -68,8 +68,13 @@ func init() {
 	httpDo = Client
 
 	// copy for InsecureTLS
-	tInsecureTLS := *t
-	tInsecureTLS.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+	tInsecureTLS := http.Transport{
+		Proxy: http.ProxyFromEnvironment,
+		Dial: func(n, a string) (net.Conn, error) {
+			return net.DialTimeout(n, a, defaultDialTimeout)
+		},
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+	}
 	ClientInsecureTLS = &http.Client{
 		Transport: &tInsecureTLS,
 	}
diff --git a/schema/pod.go b/schema/pod.go
index d4792ff..9ed6c9e 100644
--- a/schema/pod.go
+++ b/schema/pod.go
@@ -28,13 +28,15 @@ import (
 const PodManifestKind = types.ACKind("PodManifest")
 
 type PodManifest struct {
-	ACVersion   types.SemVer        `json:"acVersion"`
-	ACKind      types.ACKind        `json:"acKind"`
-	Apps        AppList             `json:"apps"`
-	Volumes     []types.Volume      `json:"volumes"`
-	Isolators   []types.Isolator    `json:"isolators"`
-	Annotations types.Annotations   `json:"annotations"`
-	Ports       []types.ExposedPort `json:"ports"`
+	ACVersion       types.SemVer          `json:"acVersion"`
+	ACKind          types.ACKind          `json:"acKind"`
+	Apps            AppList               `json:"apps"`
+	Volumes         []types.Volume        `json:"volumes"`
+	Isolators       []types.Isolator      `json:"isolators"`
+	Annotations     types.Annotations     `json:"annotations"`
+	Ports           []types.ExposedPort   `json:"ports"`
+	UserAnnotations types.UserAnnotations `json:"userAnnotations,omitempty"`
+	UserLabels      types.UserLabels      `json:"userLabels,omitempty"`
 }
 
 // podManifest is a model to facilitate extra validation during the
@@ -135,9 +137,12 @@ func (al AppList) Get(name types.ACName) *RuntimeApp {
 
 // Mount describes the mapping between a volume and the path it is mounted
 // inside of an app's filesystem.
+// The AppVolume is optional. If missing, the pod-level Volume of the
+// same name shall be used.
 type Mount struct {
-	Volume types.ACName `json:"volume"`
-	Path   string       `json:"path"`
+	Volume    types.ACName  `json:"volume"`
+	Path      string        `json:"path"`
+	AppVolume *types.Volume `json:"appVolume,omitempty"`
 }
 
 func (r Mount) assertValid() error {
diff --git a/schema/types/app.go b/schema/types/app.go
index a97ca02..0ef68c8 100644
--- a/schema/types/app.go
+++ b/schema/types/app.go
@@ -22,16 +22,18 @@ import (
 )
 
 type App struct {
-	Exec              Exec           `json:"exec"`
-	EventHandlers     []EventHandler `json:"eventHandlers,omitempty"`
-	User              string         `json:"user"`
-	Group             string         `json:"group"`
-	SupplementaryGIDs []int          `json:"supplementaryGIDs,omitempty"`
-	WorkingDirectory  string         `json:"workingDirectory,omitempty"`
-	Environment       Environment    `json:"environment,omitempty"`
-	MountPoints       []MountPoint   `json:"mountPoints,omitempty"`
-	Ports             []Port         `json:"ports,omitempty"`
-	Isolators         Isolators      `json:"isolators,omitempty"`
+	Exec              Exec            `json:"exec"`
+	EventHandlers     []EventHandler  `json:"eventHandlers,omitempty"`
+	User              string          `json:"user"`
+	Group             string          `json:"group"`
+	SupplementaryGIDs []int           `json:"supplementaryGIDs,omitempty"`
+	WorkingDirectory  string          `json:"workingDirectory,omitempty"`
+	Environment       Environment     `json:"environment,omitempty"`
+	MountPoints       []MountPoint    `json:"mountPoints,omitempty"`
+	Ports             []Port          `json:"ports,omitempty"`
+	Isolators         Isolators       `json:"isolators,omitempty"`
+	UserAnnotations   UserAnnotations `json:"userAnnotations,omitempty"`
+	UserLabels        UserLabels      `json:"userLabels,omitempty"`
 }
 
 // app is a model to facilitate extra validation during the
diff --git a/schema/types/isolator_linux_specific.go b/schema/types/isolator_linux_specific.go
index 6cb9a86..df500bd 100644
--- a/schema/types/isolator_linux_specific.go
+++ b/schema/types/isolator_linux_specific.go
@@ -17,6 +17,7 @@ package types
 import (
 	"encoding/json"
 	"errors"
+	"fmt"
 	"unicode"
 )
 
@@ -26,6 +27,8 @@ const (
 	LinuxNoNewPrivilegesName       = "os/linux/no-new-privileges"
 	LinuxSeccompRemoveSetName      = "os/linux/seccomp-remove-set"
 	LinuxSeccompRetainSetName      = "os/linux/seccomp-retain-set"
+	LinuxOOMScoreAdjName           = "os/linux/oom-score-adj"
+	LinuxCPUSharesName             = "os/linux/cpu-shares"
 )
 
 var LinuxIsolatorNames = make(map[ACIdentifier]struct{})
@@ -35,6 +38,8 @@ func init() {
 		LinuxCapabilitiesRevokeSetName: func() IsolatorValue { return &LinuxCapabilitiesRevokeSet{} },
 		LinuxCapabilitiesRetainSetName: func() IsolatorValue { return &LinuxCapabilitiesRetainSet{} },
 		LinuxNoNewPrivilegesName:       func() IsolatorValue { v := LinuxNoNewPrivileges(false); return &v },
+		LinuxOOMScoreAdjName:           func() IsolatorValue { v := LinuxOOMScoreAdj(0); return &v },
+		LinuxCPUSharesName:             func() IsolatorValue { v := LinuxCPUShares(1024); return &v },
 		LinuxSeccompRemoveSetName:      func() IsolatorValue { return &LinuxSeccompRemoveSet{} },
 		LinuxSeccompRetainSetName:      func() IsolatorValue { return &LinuxSeccompRetainSet{} },
 	} {
@@ -321,3 +326,107 @@ func (l LinuxSeccompRemoveSet) AsIsolator() (*Isolator, error) {
 		value:    &l,
 	}, nil
 }
+
+// LinuxCPUShares assigns the CPU time share weight to the processes executed.
+// See https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html#CPUShares=weight,
+// https://www.kernel.org/doc/Documentation/scheduler/sched-design-CFS.txt
+type LinuxCPUShares int
+
+func NewLinuxCPUShares(val int) (*LinuxCPUShares, error) {
+	l := LinuxCPUShares(val)
+	if err := l.AssertValid(); err != nil {
+		return nil, err
+	}
+
+	return &l, nil
+}
+
+func (l LinuxCPUShares) AssertValid() error {
+	if l < 2 || l > 262144 {
+		return fmt.Errorf("%s must be between 2 and 262144, got %d", LinuxCPUSharesName, l)
+	}
+	return nil
+}
+
+func (l LinuxCPUShares) multipleAllowed() bool {
+	return false
+}
+
+func (l LinuxCPUShares) Conflicts() []ACIdentifier {
+	return nil
+}
+
+func (l *LinuxCPUShares) UnmarshalJSON(b []byte) error {
+	var v int
+	err := json.Unmarshal(b, &v)
+	if err != nil {
+		return err
+	}
+
+	*l = LinuxCPUShares(v)
+	return nil
+}
+
+func (l LinuxCPUShares) AsIsolator() Isolator {
+	b, err := json.Marshal(l)
+	if err != nil {
+		panic(err)
+	}
+	rm := json.RawMessage(b)
+	return Isolator{
+		Name:     LinuxCPUSharesName,
+		ValueRaw: &rm,
+		value:    &l,
+	}
+}
+
+// LinuxOOMScoreAdj is equivalent to /proc/[pid]/oom_score_adj
+type LinuxOOMScoreAdj int // -1000 to 1000
+
+func NewLinuxOOMScoreAdj(val int) (*LinuxOOMScoreAdj, error) {
+	l := LinuxOOMScoreAdj(val)
+	if err := l.AssertValid(); err != nil {
+		return nil, err
+	}
+
+	return &l, nil
+}
+
+func (l LinuxOOMScoreAdj) AssertValid() error {
+	if l < -1000 || l > 1000 {
+		return fmt.Errorf("%s must be between -1000 and 1000, got %d", LinuxOOMScoreAdjName, l)
+	}
+	return nil
+}
+
+func (l LinuxOOMScoreAdj) multipleAllowed() bool {
+	return false
+}
+
+func (l LinuxOOMScoreAdj) Conflicts() []ACIdentifier {
+	return nil
+}
+
+func (l *LinuxOOMScoreAdj) UnmarshalJSON(b []byte) error {
+	var v int
+	err := json.Unmarshal(b, &v)
+	if err != nil {
+		return err
+	}
+
+	*l = LinuxOOMScoreAdj(v)
+	return nil
+}
+
+func (l LinuxOOMScoreAdj) AsIsolator() Isolator {
+	b, err := json.Marshal(l)
+	if err != nil {
+		panic(err)
+	}
+	rm := json.RawMessage(b)
+	return Isolator{
+		Name:     LinuxOOMScoreAdjName,
+		ValueRaw: &rm,
+		value:    &l,
+	}
+}
diff --git a/schema/types/mountpoint.go b/schema/types/mountpoint.go
index 80eab94..9d4e52b 100644
--- a/schema/types/mountpoint.go
+++ b/schema/types/mountpoint.go
@@ -23,6 +23,7 @@ import (
 	"github.com/appc/spec/schema/common"
 )
 
+// MountPoint is the application-side manifestation of a Volume.
 type MountPoint struct {
 	Name     ACName `json:"name"`
 	Path     string `json:"path"`
diff --git a/schema/types/port.go b/schema/types/port.go
index 78d6de4..2d54515 100644
--- a/schema/types/port.go
+++ b/schema/types/port.go
@@ -18,12 +18,15 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"net"
 	"net/url"
 	"strconv"
 
 	"github.com/appc/spec/schema/common"
 )
 
+// Port represents a port as offered by an application *inside*
+// the pod.
 type Port struct {
 	Name            ACName `json:"name"`
 	Protocol        string `json:"protocol"`
@@ -32,9 +35,14 @@ type Port struct {
 	SocketActivated bool   `json:"socketActivated"`
 }
 
+// ExposedPort represents a port listening on the host side.
+// The PodPort is optional -- if missing, then try and find the pod-side
+// information by matching names
 type ExposedPort struct {
 	Name     ACName `json:"name"`
 	HostPort uint   `json:"hostPort"`
+	HostIP   net.IP `json:"hostIP,omitempty"`  // optional
+	PodPort  *Port  `json:"podPort,omitempty"` // optional. If missing, try and find a corresponding App's port
 }
 
 type port Port
diff --git a/schema/types/user_annotations.go b/schema/types/user_annotations.go
new file mode 100644
index 0000000..63ba434
--- /dev/null
+++ b/schema/types/user_annotations.go
@@ -0,0 +1,18 @@
+// Copyright 2016 The appc Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package types
+
+// UserAnnotations are arbitrary key-value pairs, to be supplied and interpreted by the user
+type UserAnnotations map[string]string
diff --git a/schema/types/user_labels.go b/schema/types/user_labels.go
new file mode 100644
index 0000000..5ed927c
--- /dev/null
+++ b/schema/types/user_labels.go
@@ -0,0 +1,18 @@
+// Copyright 2015 The appc Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package types
+
+// UserLabels are arbitrary key-value pairs, to be supplied and interpreted by the user
+type UserLabels map[string]string
diff --git a/schema/types/volume.go b/schema/types/volume.go
index c2c429d..0986a61 100644
--- a/schema/types/volume.go
+++ b/schema/types/volume.go
@@ -156,8 +156,6 @@ func (v Volume) String() string {
 // Example volume parameters:
 // 	database,kind=host,source=/tmp,readOnly=true,recursive=true
 func VolumeFromString(vp string) (*Volume, error) {
-	var vol Volume
-
 	vp = "name=" + vp
 	vpQuery, err := common.MakeQueryString(vp)
 	if err != nil {
@@ -168,7 +166,12 @@ func VolumeFromString(vp string) (*Volume, error) {
 	if err != nil {
 		return nil, err
 	}
-	for key, val := range v {
+	return VolumeFromParams(v)
+}
+
+func VolumeFromParams(params map[string][]string) (*Volume, error) {
+	var vol Volume
+	for key, val := range params {
 		val := val
 		if len(val) > 1 {
 			return nil, fmt.Errorf("label %s with multiple values %q", key, val)
@@ -218,8 +221,7 @@ func VolumeFromString(vp string) (*Volume, error) {
 
 	maybeSetDefaults(&vol)
 
-	err = vol.assertValid()
-	if err != nil {
+	if err := vol.assertValid(); err != nil {
 		return nil, err
 	}
 


Test patch:
diff --git a/schema/types/app_test.go b/schema/types/app_test.go
index cd056d6..ad5ee6f 100644
--- a/schema/types/app_test.go
+++ b/schema/types/app_test.go
@@ -211,6 +211,24 @@ func TestAppUnmarshal(t *testing.T) {
 			},
 			false,
 		},
+		{
+			`{"Exec":["/a"],"User":"0","Group":"0","UserAnnotations":{"weird!":"normal?"},"UserLabels":{"one!":"two?"}}`,
+			&App{
+				Exec: Exec{
+					"/a",
+				},
+				User:        "0",
+				Group:       "0",
+				Environment: make(Environment, 0),
+				UserAnnotations: UserAnnotations{
+					"weird!": "normal?",
+				},
+				UserLabels: UserLabels{
+					"one!": "two?",
+				},
+			},
+			false,
+		},
 	}
 	for i, tt := range tests {
 		a := &App{}
diff --git a/schema/types/isolator_test.go b/schema/types/isolator_test.go
index 3a317e9..5e74ce9 100644
--- a/schema/types/isolator_test.go
+++ b/schema/types/isolator_test.go
@@ -25,6 +25,69 @@ func TestIsolatorUnmarshal(t *testing.T) {
 		msg  string
 		werr bool
 	}{
+		{
+			`{
+				"name": "os/linux/cpu-shares",
+				"value": 2048
+			}`,
+			false,
+		},
+		{
+			`{
+				"name": "os/linux/cpu-shares",
+				"value": 1
+			}`,
+			true,
+		},
+		{
+			`{
+				"name": "os/linux/cpu-shares",
+				"value": "pants"
+			}`,
+			true,
+		},
+		{
+			`{
+				"name": "os/linux/cpu-shares",
+				"value": "262145"
+			}`,
+			true,
+		},
+		{
+			`{
+				"name": "os/linux/cpu-shares",
+				"value": "-1"
+			}`,
+			true,
+		},
+		{
+			`{
+				"name": "os/linux/oom-score-adj",
+				"value": 250
+			}`,
+			false,
+		},
+		{
+			`{
+				"name": "os/linux/oom-score-adj",
+				"value": -250
+			}`,
+			false,
+		},
+		{
+			`{
+				"name": "os/linux/oom-score-adj",
+				"value": -2500
+			}`,
+			true,
+		},
+		{
+			`{
+				"name": "os/linux/oom-score-adj",
+				"value": "pants"
+			}`,
+			true,
+		},
 		{
 			`{
 				"name": "os/linux/no-new-privileges",