manifest: remove links from services (#41)

* remove links from services

* make internal service discovery use one port for less confusion

* lint and cleanup

* use correct env priority on one-offs
This commit is contained in:
David Dollar 2019-12-05 08:16:21 -05:00 committed by GitHub
parent 3bd48fc505
commit 454d28fbf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 159 additions and 168 deletions

View File

@ -36,7 +36,6 @@ For an app named `myapp` with a `convox.yml` like this:
internal: true
ports:
- 5000
- 5001
web:
port: 3000
@ -44,16 +43,15 @@ You would see a `convox services` output similar to this:
$ convox services
SERVICE DOMAIN PORTS
auth auth.convox-myapp.svc.cluster.local 5000 5001
auth auth.convox-myapp.svc.cluster.local 5000
web web.myapp.0a1b2c3d4e5f.convox.cloud 443:3000
The `web` Service could reach the `auth` Service using the following endpoints:
The `web` Service could reach the `auth` Service using the following URL:
* `auth.convox-myapp.svc.cluster.local:5000`
* `auth.convox-myapp.svc.cluster.local:5001`
* `http://auth.convox-myapp.svc.cluster.local:5000`
> Note that the internal `auth` Service is no longer receiving automatic HTTPS termination. If you want this
> connection to be encrypted you would need to terminate HTTPS inside your Service.
> Note that the internal port of the `auth` Service is not receiving automatic SSL termination.
> If you want this connection to be encrypted you would need to handle SSL inside the Service.
DNS search suffixes are automatically configured for internal hostnames on a Rack. The following URLs would
also work for contacting the `auth` Service:
@ -61,5 +59,5 @@ also work for contacting the `auth` Service:
* `http://auth:5000` for Services on the same app.
* `http://auth.convox-myapp:5000` for other apps on the same Rack.
> The `convox` portion of the internal hostnames in the examples above is the name of the Rack.
> The `convox` portion of these internal hostnames is the name of the Rack.
> You can find the name of a Rack using `convox rack`.

View File

@ -1,20 +1,9 @@
package manifest
import (
"math/rand"
"regexp"
)
func coalesce(ss ...string) string {
for _, s := range ss {
if s != "" {
return s
}
}
return ""
}
var regexpInterpolation = regexp.MustCompile(`\$\{([^}]*?)\}`)
func interpolate(data []byte, env map[string]string) ([]byte, error) {
@ -24,13 +13,3 @@ func interpolate(data []byte, env map[string]string) ([]byte, error) {
return p, nil
}
var randomAlphabet = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
func randomString(size int) string {
b := make([]rune, size)
for i := range b {
b[i] = randomAlphabet[rand.Intn(len(randomAlphabet))]
}
return string(b)
}

View File

@ -2,7 +2,6 @@ package manifest
import (
"fmt"
"io"
"math/rand"
"sort"
"strings"
@ -63,7 +62,7 @@ func Load(data []byte, env map[string]string) (*Manifest, error) {
return nil, err
}
if err := m.ValidateEnv(); err != nil {
if err := m.Validate(); err != nil {
return nil, err
}
@ -82,6 +81,68 @@ func (m *Manifest) Agents() []string {
return a
}
func (m *Manifest) ApplyDefaults() error {
for i, s := range m.Services {
if s.Build.Path == "" && s.Image == "" {
m.Services[i].Build.Path = "."
}
if m.Services[i].Build.Path != "" && s.Build.Manifest == "" {
m.Services[i].Build.Manifest = "Dockerfile"
}
if s.Drain == 0 {
m.Services[i].Drain = 30
}
if s.Health.Path == "" {
m.Services[i].Health.Path = "/"
}
if s.Health.Interval == 0 {
m.Services[i].Health.Interval = 5
}
if s.Health.Grace == 0 {
m.Services[i].Health.Grace = m.Services[i].Health.Interval
}
if s.Health.Timeout == 0 {
m.Services[i].Health.Timeout = m.Services[i].Health.Interval - 1
}
if s.Port.Port > 0 && s.Port.Scheme == "" {
m.Services[i].Port.Scheme = "http"
}
sp := fmt.Sprintf("services.%s.scale", s.Name)
// if no scale attributes set
if len(m.AttributesByPrefix(sp)) == 0 {
m.Services[i].Scale.Count = ServiceScaleCount{Min: 1, Max: 1}
}
// if no explicit count attribute set yet has multiple scale attributes other than count
if !m.AttributeExists(fmt.Sprintf("%s.count", sp)) && len(m.AttributesByPrefix(sp)) > 1 {
m.Services[i].Scale.Count = ServiceScaleCount{Min: 1, Max: 1}
}
if m.Services[i].Scale.Cpu == 0 {
m.Services[i].Scale.Cpu = DefaultCpu
}
if m.Services[i].Scale.Memory == 0 {
m.Services[i].Scale.Memory = DefaultMem
}
if !m.AttributeExists(fmt.Sprintf("services.%s.sticky", s.Name)) {
m.Services[i].Sticky = true
}
}
return nil
}
func (m *Manifest) Attributes() []string {
attrs := []string{}
@ -106,7 +167,7 @@ func (m *Manifest) AttributesByPrefix(prefix string) []string {
return attrs
}
func (m *Manifest) AttributeSet(name string) bool {
func (m *Manifest) AttributeExists(name string) bool {
return m.attributes[name]
}
@ -114,20 +175,9 @@ func (m *Manifest) Env() map[string]string {
return m.env
}
// used only for tests
func (m *Manifest) SetAttributes(attrs []string) {
m.attributes = map[string]bool{}
for _, a := range attrs {
m.attributes[a] = true
}
}
// used only for tests
func (m *Manifest) SetEnv(env map[string]string) {
m.env = env
}
// CombineEnv calculates the final environment of each service
// and filters m.env to the union of all service env vars
// defined in the manifest
func (m *Manifest) CombineEnv() error {
for i, s := range m.Services {
me := make([]string, len(m.Environment))
@ -135,6 +185,25 @@ func (m *Manifest) CombineEnv() error {
m.Services[i].Environment = append(me, s.Environment...)
}
keys := map[string]bool{}
for _, s := range m.Services {
env, err := m.ServiceEnvironment(s.Name)
if err != nil {
return err
}
for k := range env {
keys[k] = true
}
}
for k := range m.env {
if !keys[k] {
delete(m.env, k)
}
}
return nil
}
@ -195,95 +264,16 @@ func (m *Manifest) ServiceEnvironment(service string) (map[string]string, error)
return env, nil
}
// ValidateEnv returns an error if required env vars for a service are not available
// It also filters m.env to the union of all service env vars defined in the manifest
func (m *Manifest) ValidateEnv() error {
keys := map[string]bool{}
// used only for tests
func (m *Manifest) SetAttributes(attrs []string) {
m.attributes = map[string]bool{}
for _, s := range m.Services {
env, err := m.ServiceEnvironment(s.Name)
if err != nil {
return err
}
for k := range env {
keys[k] = true
}
}
for k := range m.env {
if !keys[k] {
delete(m.env, k)
}
}
return nil
}
func (m *Manifest) ApplyDefaults() error {
for i, s := range m.Services {
if s.Build.Path == "" && s.Image == "" {
m.Services[i].Build.Path = "."
}
if m.Services[i].Build.Path != "" && s.Build.Manifest == "" {
m.Services[i].Build.Manifest = "Dockerfile"
}
if s.Drain == 0 {
m.Services[i].Drain = 30
}
if s.Health.Path == "" {
m.Services[i].Health.Path = "/"
}
if s.Health.Interval == 0 {
m.Services[i].Health.Interval = 5
}
if s.Health.Grace == 0 {
m.Services[i].Health.Grace = m.Services[i].Health.Interval
}
if s.Health.Timeout == 0 {
m.Services[i].Health.Timeout = m.Services[i].Health.Interval - 1
}
if s.Port.Port > 0 && s.Port.Scheme == "" {
m.Services[i].Port.Scheme = "http"
}
sp := fmt.Sprintf("services.%s.scale", s.Name)
// if no scale attributes set
if len(m.AttributesByPrefix(sp)) == 0 {
m.Services[i].Scale.Count = ServiceScaleCount{Min: 1, Max: 1}
}
// if no explicit count attribute set yet has multiple scale attributes other than count
if !m.AttributeSet(fmt.Sprintf("%s.count", sp)) && len(m.AttributesByPrefix(sp)) > 1 {
m.Services[i].Scale.Count = ServiceScaleCount{Min: 1, Max: 1}
}
if m.Services[i].Scale.Cpu == 0 {
m.Services[i].Scale.Cpu = DefaultCpu
}
if m.Services[i].Scale.Memory == 0 {
m.Services[i].Scale.Memory = DefaultMem
}
if !m.AttributeSet(fmt.Sprintf("services.%s.sticky", s.Name)) {
m.Services[i].Sticky = true
}
}
return nil
}
func message(w io.Writer, format string, args ...interface{}) {
if w != nil {
w.Write([]byte(fmt.Sprintf(format, args...) + "\n"))
for _, a := range attrs {
m.attributes[a] = true
}
}
// used only for tests
func (m *Manifest) SetEnv(env map[string]string) {
m.env = env
}

View File

@ -329,7 +329,6 @@ func TestManifestLoad(t *testing.T) {
// env processing that normally happens as part of load
require.NoError(t, n.CombineEnv())
require.NoError(t, n.ValidateEnv())
m, err := testdataManifest("full", env)
require.NoError(t, err)
@ -388,7 +387,6 @@ func TestManifestLoadSimple(t *testing.T) {
// env processing that normally happens as part of load
require.NoError(t, n.CombineEnv())
require.NoError(t, n.ValidateEnv())
m, err := testdataManifest("simple", map[string]string{"REQUIRED": "test"})
require.NoError(t, err)
@ -414,6 +412,7 @@ func TestManifestLoadInvalid(t *testing.T) {
m, err = testdataManifest("invalid.2", map[string]string{})
require.NotNil(t, m)
require.NoError(t, err)
require.Len(t, m.Services, 0)
}

View File

@ -20,7 +20,6 @@ type Service struct {
Image string `yaml:"image,omitempty"`
Init bool `yaml:"init,omitempty"`
Internal bool `yaml:"internal,omitempty"`
Links []string `yaml:"links,omitempty"`
Port ServicePortScheme `yaml:"port,omitempty"`
Ports []ServicePortProtocol `yaml:"ports,omitempty"`
Privileged bool `yaml:"privileged,omitempty"`

38
pkg/manifest/validate.go Normal file
View File

@ -0,0 +1,38 @@
package manifest
import (
"fmt"
"strings"
)
func (m *Manifest) Validate() error {
if err := m.validateEnv(); err != nil {
return err
}
if err := m.validateResources(); err != nil {
return err
}
return nil
}
func (m *Manifest) validateEnv() error {
for _, s := range m.Services {
if _, err := m.ServiceEnvironment(s.Name); err != nil {
return err
}
}
return nil
}
func (m *Manifest) validateResources() error {
for _, r := range m.Resources {
if strings.TrimSpace(r.Type) == "" {
return fmt.Errorf("resource %q has blank type", r.Name)
}
}
return nil
}

View File

@ -447,11 +447,11 @@ func (v *ServiceScaleCount) UnmarshalYAML(unmarshal func(interface{}) error) err
}
case map[interface{}]interface{}:
if min := t["min"]; min != nil {
switch min.(type) {
switch u := min.(type) {
case int:
v.Min = min.(int)
v.Min = u
case string:
mins, err := strconv.Atoi(min.(string))
mins, err := strconv.Atoi(u)
if err != nil {
return err
}
@ -461,11 +461,11 @@ func (v *ServiceScaleCount) UnmarshalYAML(unmarshal func(interface{}) error) err
}
}
if max := t["max"]; max != nil {
switch max.(type) {
switch u := max.(type) {
case int:
v.Max = max.(int)
v.Max = u
case string:
maxs, err := strconv.Atoi(max.(string))
maxs, err := strconv.Atoi(u)
if err != nil {
return err
}

View File

@ -350,27 +350,17 @@ func (p *Provider) podSpecFromService(app, service, release string) (*ac.PodSpec
return nil, err
}
env := map[string]string{}
senv, err := p.systemEnvironment(app, release)
if err != nil {
return nil, err
}
env := map[string]string{}
for k, v := range senv {
env[k] = v
}
e := structs.Environment{}
if err := e.Load([]byte(r.Env)); err != nil {
return nil, err
}
for k, v := range e {
env[k] = v
}
if s, _ := m.Service(service); s != nil {
if s.Command != "" {
parts, err := shellquote.Split(s.Command)
@ -384,10 +374,6 @@ func (p *Provider) podSpecFromService(app, service, release string) (*ac.PodSpec
env[k] = v
}
for _, l := range s.Links {
env[fmt.Sprintf("%s_URL", envName(l))] = fmt.Sprintf("https://%s.%s.%s", l, app, p.Name)
}
for _, r := range s.Resources {
cm, err := p.Cluster.CoreV1().ConfigMaps(p.AppNamespace(app)).Get(fmt.Sprintf("resource-%s", r), am.GetOptions{})
if err != nil {
@ -421,6 +407,16 @@ func (p *Provider) podSpecFromService(app, service, release string) (*ac.PodSpec
}
}
e := structs.Environment{}
if err := e.Load([]byte(r.Env)); err != nil {
return nil, err
}
for k, v := range e {
env[k] = v
}
for k, v := range env {
c.Env = append(c.Env, ac.EnvVar{Name: k, Value: v})
}

View File

@ -80,10 +80,6 @@ spec:
valueFrom:
fieldRef:
fieldPath: status.hostIP
{{ range .Service.Links }}
- name: {{ envname . }}_URL
value: https://{{.}}.{{$.App.Name}}.{{$.Rack}}
{{ end }}
{{ range .Service.Resources }}
- name: {{ envname . }}_URL
valueFrom:

View File

@ -37,10 +37,6 @@ spec:
- {{ safe . }}
{{ end }}
env:
{{ range .Service.Links }}
- name: {{ envname . }}_URL
value: https://{{.}}.{{$.App.Name}}.{{$.Rack}}
{{ end }}
{{ range .Service.Resources }}
- name: {{ envname . }}_URL
valueFrom: