Runtime refactoring and NetworkPolicy support (#2016)

This commit is contained in:
Prawn
2020-10-14 02:54:05 +13:00
committed by GitHub
parent 5e35d89b38
commit 1a962e46fd
13 changed files with 877 additions and 362 deletions

View File

@@ -34,32 +34,52 @@ func (k *kubernetes) Init(opts ...runtime.Option) error {
return nil return nil
} }
func (k *kubernetes) Logs(s *runtime.Service, options ...runtime.LogsOption) (runtime.Logs, error) { func (k *kubernetes) Logs(resource runtime.Resource, options ...runtime.LogsOption) (runtime.Logs, error) {
klo := newLog(k.client, s.Name, options...)
if !klo.options.Stream { // Handle the various different types of resources:
records, err := klo.Read() switch resource.Type() {
case runtime.TypeNamespace:
// noop (Namespace is not supported by *kubernetes.Logs())
return nil, nil
case runtime.TypeNetworkPolicy:
// noop (NetworkPolicy is not supported by *kubernetes.Logs()))
return nil, nil
case runtime.TypeService:
// Assert the resource back into a *runtime.Service
s, ok := resource.(*runtime.Service)
if !ok {
return nil, runtime.ErrInvalidResource
}
klo := newLog(k.client, s.Name, options...)
if !klo.options.Stream {
records, err := klo.Read()
if err != nil {
log.Errorf("Failed to get logs for service '%v' from k8s: %v", s.Name, err)
return nil, err
}
kstream := &kubeStream{
stream: make(chan runtime.Log),
stop: make(chan bool),
}
go func() {
for _, record := range records {
kstream.Chan() <- record
}
kstream.Stop()
}()
return kstream, nil
}
stream, err := klo.Stream()
if err != nil { if err != nil {
log.Errorf("Failed to get logs for service '%v' from k8s: %v", s.Name, err)
return nil, err return nil, err
} }
kstream := &kubeStream{ return stream, nil
stream: make(chan runtime.Log), default:
stop: make(chan bool), return nil, runtime.ErrInvalidResource
}
go func() {
for _, record := range records {
kstream.Chan() <- record
}
kstream.Stop()
}()
return kstream, nil
} }
stream, err := klo.Stream()
if err != nil {
return nil, err
}
return stream, nil
} }
type kubeStream struct { type kubeStream struct {
@@ -92,11 +112,14 @@ func (k *kubeStream) Stop() error {
return nil return nil
} }
// Creates a service // Create a resource
func (k *kubernetes) Create(s *runtime.Service, opts ...runtime.CreateOption) error { func (k *kubernetes) Create(resource runtime.Resource, opts ...runtime.CreateOption) error {
k.Lock() k.Lock()
defer k.Unlock() defer k.Unlock()
return k.create(resource, opts...)
}
func (k *kubernetes) create(resource runtime.Resource, opts ...runtime.CreateOption) error {
// parse the options // parse the options
options := &runtime.CreateOptions{ options := &runtime.CreateOptions{
Type: k.options.Type, Type: k.options.Type,
@@ -107,47 +130,74 @@ func (k *kubernetes) Create(s *runtime.Service, opts ...runtime.CreateOption) er
o(options) o(options)
} }
// default the service's source and version // Handle the various different types of resources:
if len(s.Source) == 0 { switch resource.Type() {
s.Source = k.options.Source case runtime.TypeNamespace:
} // Assert the resource back into a *runtime.Namespace
if len(s.Version) == 0 { namespace, ok := resource.(*runtime.Namespace)
s.Version = "latest" if !ok {
} return runtime.ErrInvalidResource
// ensure the namespace exists
if err := k.ensureNamepaceExists(options.Namespace); err != nil {
return nil
}
// create a secret for the deployment
if err := k.createCredentials(s, options); err != nil {
return err
}
// create the deployment
if err := k.client.Create(client.NewDeployment(s, options), client.CreateNamespace(options.Namespace)); err != nil {
if parseError(err).Reason == "AlreadyExists" {
return runtime.ErrAlreadyExists
} }
if logger.V(logger.ErrorLevel, logger.DefaultLogger) { return k.createNamespace(namespace)
logger.Errorf("Runtime failed to create deployment: %v", err) case runtime.TypeNetworkPolicy:
// Assert the resource back into a *runtime.NetworkPolicy
networkPolicy, ok := resource.(*runtime.NetworkPolicy)
if !ok {
return runtime.ErrInvalidResource
} }
return err return k.createNetworkPolicy(networkPolicy)
} case runtime.TypeService:
// create the service, one could already exist for another version so ignore ErrAlreadyExists // Assert the resource back into a *runtime.Service
if err := k.client.Create(client.NewService(s, options), client.CreateNamespace(options.Namespace)); err != nil { s, ok := resource.(*runtime.Service)
if parseError(err).Reason == "AlreadyExists" { if !ok {
return runtime.ErrInvalidResource
}
// default the service's source and version
if len(s.Source) == 0 {
s.Source = k.options.Source
}
if len(s.Version) == 0 {
s.Version = "latest"
}
// ensure the namespace exists
if err := k.ensureNamepaceExists(options.Namespace); err != nil {
return nil return nil
} }
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("Runtime failed to create service: %v", err)
}
return err
}
return nil // create a secret for the deployment
if err := k.createCredentials(s, options); err != nil {
return err
}
// create the deployment
if err := k.client.Create(client.NewDeployment(s, options), client.CreateNamespace(options.Namespace)); err != nil {
if parseError(err).Reason == "AlreadyExists" {
return runtime.ErrAlreadyExists
}
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("Runtime failed to create deployment: %v", err)
}
return err
}
// create the service, one could already exist for another version so ignore ErrAlreadyExists
if err := k.client.Create(client.NewService(s, options), client.CreateNamespace(options.Namespace)); err != nil {
if parseError(err).Reason == "AlreadyExists" {
return nil
}
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("Runtime failed to create service: %v", err)
}
return err
}
return nil
default:
return runtime.ErrInvalidResource
}
} }
// Read returns all instances of given service // Read returns all instances of given service
@@ -180,8 +230,8 @@ func (k *kubernetes) Read(opts ...runtime.ReadOption) ([]*runtime.Service, error
return k.getServices(client.GetNamespace(options.Namespace), client.GetLabels(labels)) return k.getServices(client.GetNamespace(options.Namespace), client.GetLabels(labels))
} }
// Update the service in place // Update a resource in place
func (k *kubernetes) Update(s *runtime.Service, opts ...runtime.UpdateOption) error { func (k *kubernetes) Update(resource runtime.Resource, opts ...runtime.UpdateOption) error {
k.Lock() k.Lock()
defer k.Unlock() defer k.Unlock()
@@ -193,69 +243,91 @@ func (k *kubernetes) Update(s *runtime.Service, opts ...runtime.UpdateOption) er
o(&options) o(&options)
} }
// construct the query // Handle the various different types of resources:
labels := map[string]string{} switch resource.Type() {
if len(s.Name) > 0 { case runtime.TypeNamespace:
labels["name"] = client.Format(s.Name) // noop (Namespace is not supported by *kubernetes.Update())
} return nil
if len(s.Version) > 0 { case runtime.TypeNetworkPolicy:
labels["version"] = client.Format(s.Version) // Assert the resource back into a *runtime.NetworkPolicy
} networkPolicy, ok := resource.(*runtime.NetworkPolicy)
if !ok {
return runtime.ErrInvalidResource
}
return k.updateNetworkPolicy(networkPolicy)
case runtime.TypeService:
// get the existing deployments // Assert the resource back into a *runtime.Service
depList := new(client.DeploymentList) s, ok := resource.(*runtime.Service)
d := &client.Resource{ if !ok {
Kind: "deployment", return runtime.ErrInvalidResource
Value: depList,
}
depOpts := []client.GetOption{
client.GetNamespace(options.Namespace),
client.GetLabels(labels),
}
if err := k.client.Get(d, depOpts...); err != nil {
return err
} else if len(depList.Items) == 0 {
return runtime.ErrNotFound
}
// update the deployments which match the query
for _, dep := range depList.Items {
// the service wan't created by the k8s runtime
if dep.Metadata == nil || dep.Metadata.Annotations == nil {
continue
} }
// update metadata // construct the query
for k, v := range s.Metadata { labels := map[string]string{}
dep.Metadata.Annotations[k] = v if len(s.Name) > 0 {
labels["name"] = client.Format(s.Name)
}
if len(s.Version) > 0 {
labels["version"] = client.Format(s.Version)
} }
// update build time annotation // get the existing deployments
dep.Spec.Template.Metadata.Annotations["updated"] = fmt.Sprintf("%d", time.Now().Unix()) depList := new(client.DeploymentList)
d := &client.Resource{
// update the deployment
res := &client.Resource{
Kind: "deployment", Kind: "deployment",
Name: resourceName(s), Value: depList,
Value: &dep,
} }
if err := k.client.Update(res, client.UpdateNamespace(options.Namespace)); err != nil { depOpts := []client.GetOption{
if logger.V(logger.ErrorLevel, logger.DefaultLogger) { client.GetNamespace(options.Namespace),
logger.Errorf("Runtime failed to update deployment: %v", err) client.GetLabels(labels),
} }
if err := k.client.Get(d, depOpts...); err != nil {
return err return err
} else if len(depList.Items) == 0 {
return runtime.ErrNotFound
} }
}
return nil // update the deployments which match the query
for _, dep := range depList.Items {
// the service wan't created by the k8s runtime
if dep.Metadata == nil || dep.Metadata.Annotations == nil {
continue
}
// update metadata
for k, v := range s.Metadata {
dep.Metadata.Annotations[k] = v
}
// update build time annotation
dep.Spec.Template.Metadata.Annotations["updated"] = fmt.Sprintf("%d", time.Now().Unix())
// update the deployment
res := &client.Resource{
Kind: "deployment",
Name: resourceName(s),
Value: &dep,
}
if err := k.client.Update(res, client.UpdateNamespace(options.Namespace)); err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("Runtime failed to update deployment: %v", err)
}
return err
}
}
return nil
default:
return runtime.ErrInvalidResource
}
} }
// Delete removes a service // Delete removes a resource
func (k *kubernetes) Delete(s *runtime.Service, opts ...runtime.DeleteOption) error { func (k *kubernetes) Delete(resource runtime.Resource, opts ...runtime.DeleteOption) error {
k.Lock() k.Lock()
defer k.Unlock() defer k.Unlock()
// parse the options
options := runtime.DeleteOptions{ options := runtime.DeleteOptions{
Namespace: client.DefaultNamespace, Namespace: client.DefaultNamespace,
} }
@@ -263,51 +335,78 @@ func (k *kubernetes) Delete(s *runtime.Service, opts ...runtime.DeleteOption) er
o(&options) o(&options)
} }
// delete the deployment // Handle the various different types of resources:
dep := client.NewDeployment(s, &runtime.CreateOptions{ switch resource.Type() {
Type: k.options.Type, case runtime.TypeNamespace:
Namespace: options.Namespace, // Assert the resource back into a *runtime.Namespace
}) namespace, ok := resource.(*runtime.Namespace)
if err := k.client.Delete(dep, client.DeleteNamespace(options.Namespace)); err != nil { if !ok {
if err == api.ErrNotFound { return runtime.ErrInvalidResource
return runtime.ErrNotFound
} }
if logger.V(logger.ErrorLevel, logger.DefaultLogger) { return k.deleteNamespace(namespace)
logger.Errorf("Runtime failed to delete deployment: %v", err) case runtime.TypeNetworkPolicy:
// Assert the resource back into a *runtime.NetworkPolicy
networkPolicy, ok := resource.(*runtime.NetworkPolicy)
if !ok {
return runtime.ErrInvalidResource
} }
return err return k.deleteNetworkPolicy(networkPolicy)
} case runtime.TypeService:
// delete the credentials // Assert the resource back into a *runtime.Service
if err := k.deleteCredentials(s, &runtime.CreateOptions{Namespace: options.Namespace}); err != nil { s, ok := resource.(*runtime.Service)
return err if !ok {
} return runtime.ErrInvalidResource
// if there are more deployments for this service, then don't delete it
labels := map[string]string{}
if len(s.Name) > 0 {
labels["name"] = client.Format(s.Name)
}
// get the existing services. todo: refactor to just get the deployments
services, err := k.getServices(client.GetNamespace(options.Namespace), client.GetLabels(labels))
if err != nil || len(services) > 0 {
return err
}
// delete the service
srv := client.NewService(s, &runtime.CreateOptions{
Type: k.options.Type,
Namespace: options.Namespace,
})
if err := k.client.Delete(srv, client.DeleteNamespace(options.Namespace)); err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("Runtime failed to delete service: %v", err)
} }
return err
}
return nil // delete the deployment
dep := client.NewDeployment(s, &runtime.CreateOptions{
Type: k.options.Type,
Namespace: options.Namespace,
})
if err := k.client.Delete(dep, client.DeleteNamespace(options.Namespace)); err != nil {
if err == api.ErrNotFound {
return runtime.ErrNotFound
}
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("Runtime failed to delete deployment: %v", err)
}
return err
}
// delete the credentials
if err := k.deleteCredentials(s, &runtime.CreateOptions{Namespace: options.Namespace}); err != nil {
return err
}
// if there are more deployments for this service, then don't delete it
labels := map[string]string{}
if len(s.Name) > 0 {
labels["name"] = client.Format(s.Name)
}
// get the existing services. todo: refactor to just get the deployments
services, err := k.getServices(client.GetNamespace(options.Namespace), client.GetLabels(labels))
if err != nil || len(services) > 0 {
return err
}
// delete the service
srv := client.NewService(s, &runtime.CreateOptions{
Type: k.options.Type,
Namespace: options.Namespace,
})
if err := k.client.Delete(srv, client.DeleteNamespace(options.Namespace)); err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("Runtime failed to delete service: %v", err)
}
return err
}
return nil
default:
return runtime.ErrInvalidResource
}
} }
// Start starts the runtime // Start starts the runtime

View File

@@ -11,6 +11,9 @@ import (
"regexp" "regexp"
"strings" "strings"
"testing" "testing"
"github.com/micro/go-micro/v3/runtime"
"github.com/stretchr/testify/assert"
) )
func setupClient(t *testing.T) { func setupClient(t *testing.T) {
@@ -47,19 +50,45 @@ func setupClient(t *testing.T) {
func TestNamespaceCreateDelete(t *testing.T) { func TestNamespaceCreateDelete(t *testing.T) {
defer func() { defer func() {
exec.Command("kubectl", "-n", "foobar", "delete", "networkpolicy", "baz").Run()
exec.Command("kubectl", "delete", "namespace", "foobar").Run() exec.Command("kubectl", "delete", "namespace", "foobar").Run()
}() }()
setupClient(t) setupClient(t)
r := NewRuntime() r := NewRuntime()
if err := r.CreateNamespace("foobar"); err != nil {
t.Fatalf("Unexpected error creating namespace %s", err) // Create a namespace
testNamespace, err := runtime.NewNamespace("foobar")
assert.NoError(t, err)
if err := r.Create(testNamespace); err != nil {
t.Fatalf("Unexpected error creating namespace: %v", err)
} }
// Check that the namespace exists
if !namespaceExists(t, "foobar") { if !namespaceExists(t, "foobar") {
t.Fatalf("Namespace foobar not found") t.Fatalf("Namespace foobar not found")
} }
if err := r.DeleteNamespace("foobar"); err != nil {
t.Fatalf("Unexpected error deleting namespace %s", err) // Create a networkpolicy:
testNetworkPolicy, err := runtime.NewNetworkPolicy("baz", "foobar", nil)
assert.NoError(t, err)
if err := r.Create(testNetworkPolicy); err != nil {
t.Fatalf("Unexpected error creating networkpolicy: %v", err)
}
// Check that the networkpolicy exists:
if !networkPolicyExists(t, "foobar", "baz") {
t.Fatalf("NetworkPolicy foobar.baz not found")
}
// Tidy up
if err := r.Delete(testNetworkPolicy); err != nil {
t.Fatalf("Unexpected error deleting networkpolicy: %v", err)
}
if networkPolicyExists(t, "foobar", "baz") {
t.Fatalf("NetworkPolicy foobar.baz still exists")
}
if err := r.Delete(testNamespace); err != nil {
t.Fatalf("Unexpected error deleting namespace: %v", err)
} }
if namespaceExists(t, "foobar") { if namespaceExists(t, "foobar") {
t.Fatalf("Namespace foobar still exists") t.Fatalf("Namespace foobar still exists")
@@ -77,5 +106,17 @@ func namespaceExists(t *testing.T, ns string) bool {
t.Fatalf("Error listing namespaces %s", err) t.Fatalf("Error listing namespaces %s", err)
} }
return exists return exists
}
func networkPolicyExists(t *testing.T, ns, np string) bool {
cmd := exec.Command("kubectl", "-n", ns, "get", "networkpolicy")
outp, err := cmd.Output()
if err != nil {
t.Fatalf("Unexpected error listing networkpolicies %s", err)
}
exists, err := regexp.Match(np, outp)
if err != nil {
t.Fatalf("Error listing networkpolicies %s", err)
}
return exists
} }

View File

@@ -4,6 +4,7 @@ import (
"strings" "strings"
"github.com/micro/go-micro/v3/logger" "github.com/micro/go-micro/v3/logger"
"github.com/micro/go-micro/v3/runtime"
"github.com/micro/go-micro/v3/util/kubernetes/client" "github.com/micro/go-micro/v3/util/kubernetes/client"
) )
@@ -24,7 +25,7 @@ func (k *kubernetes) ensureNamepaceExists(ns string) error {
return err return err
} }
if err := k.createNamespace(namespace); err != nil { if err := k.autoCreateNamespace(namespace); err != nil {
if logger.V(logger.WarnLevel, logger.DefaultLogger) { if logger.V(logger.WarnLevel, logger.DefaultLogger) {
logger.Warnf("Error creating namespace %v: %v", namespace, err) logger.Warnf("Error creating namespace %v: %v", namespace, err)
} }
@@ -64,8 +65,8 @@ func (k *kubernetes) namespaceExists(name string) (bool, error) {
return false, nil return false, nil
} }
// createNamespace creates a new k8s namespace // autoCreateNamespace creates a new k8s namespace
func (k *kubernetes) createNamespace(namespace string) error { func (k *kubernetes) autoCreateNamespace(namespace string) error {
ns := client.Namespace{Metadata: &client.Metadata{Name: namespace}} ns := client.Namespace{Metadata: &client.Metadata{Name: namespace}}
err := k.client.Create(&client.Resource{Kind: "namespace", Value: ns}) err := k.client.Create(&client.Resource{Kind: "namespace", Value: ns})
@@ -75,38 +76,54 @@ func (k *kubernetes) createNamespace(namespace string) error {
err = nil err = nil
} }
// add to cache // add to cache and create networkpolicy
if err == nil && k.namespaces != nil { if err == nil && k.namespaces != nil {
k.namespaces = append(k.namespaces, ns) k.namespaces = append(k.namespaces, ns)
if networkPolicy, err := runtime.NewNetworkPolicy("ingress", namespace, map[string]string{"owner": "micro"}); err != nil {
return err
} else {
return k.create(networkPolicy)
}
} }
return err return err
} }
func (k *kubernetes) CreateNamespace(ns string) error { // createNamespace creates a namespace resource
func (k *kubernetes) createNamespace(namespace *runtime.Namespace) error {
err := k.client.Create(&client.Resource{ err := k.client.Create(&client.Resource{
Kind: "namespace", Kind: "namespace",
Name: namespace.Name,
Value: client.Namespace{ Value: client.Namespace{
Metadata: &client.Metadata{ Metadata: &client.Metadata{
Name: ns, Name: namespace.Name,
}, },
}, },
}) }, client.CreateNamespace(namespace.Name))
if err != nil { if err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) { if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("Error creating namespace %v: %v", ns, err) logger.Errorf("Error creating namespace %s: %v", namespace.String(), err)
} }
} }
return err return err
} }
func (k *kubernetes) DeleteNamespace(ns string) error { // deleteNamespace deletes a namespace resource
func (k *kubernetes) deleteNamespace(namespace *runtime.Namespace) error {
err := k.client.Delete(&client.Resource{ err := k.client.Delete(&client.Resource{
Kind: "namespace", Kind: "namespace",
Name: ns, Name: namespace.Name,
}) Value: client.Namespace{
if err != nil && logger.V(logger.ErrorLevel, logger.DefaultLogger) { Metadata: &client.Metadata{
logger.Errorf("Error deleting namespace %v: %v", ns, err) Name: namespace.Name,
},
},
}, client.DeleteNamespace(namespace.Name))
if err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("Error deleting namespace %s: %v", namespace.String(), err)
}
} }
return err return err
} }

View File

@@ -0,0 +1,67 @@
package kubernetes
import (
"github.com/micro/go-micro/v3/logger"
"github.com/micro/go-micro/v3/runtime"
"github.com/micro/go-micro/v3/util/kubernetes/client"
)
// createNetworkPolicy creates a networkpolicy resource
func (k *kubernetes) createNetworkPolicy(networkPolicy *runtime.NetworkPolicy) error {
err := k.client.Create(&client.Resource{
Kind: "networkpolicy",
Value: client.NetworkPolicy{
AllowedLabels: networkPolicy.AllowedLabels,
Metadata: &client.Metadata{
Name: networkPolicy.Name,
Namespace: networkPolicy.Namespace,
},
},
}, client.CreateNamespace(networkPolicy.Namespace))
if err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("Error creating resource %s: %v", networkPolicy.String(), err)
}
}
return err
}
// updateNetworkPolicy updates a networkpolicy resource in-place
func (k *kubernetes) updateNetworkPolicy(networkPolicy *runtime.NetworkPolicy) error {
err := k.client.Update(&client.Resource{
Kind: "networkpolicy",
Value: client.NetworkPolicy{
AllowedLabels: networkPolicy.AllowedLabels,
Metadata: &client.Metadata{
Name: networkPolicy.Name,
Namespace: networkPolicy.Namespace,
},
},
}, client.UpdateNamespace(networkPolicy.Namespace))
if err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("Error updating resource %s: %v", networkPolicy.String(), err)
}
}
return err
}
// deleteNetworkPolicy deletes a networkpolicy resource
func (k *kubernetes) deleteNetworkPolicy(networkPolicy *runtime.NetworkPolicy) error {
err := k.client.Delete(&client.Resource{
Kind: "networkpolicy",
Value: client.NetworkPolicy{
AllowedLabels: networkPolicy.AllowedLabels,
Metadata: &client.Metadata{
Name: networkPolicy.Name,
Namespace: networkPolicy.Namespace,
},
},
}, client.DeleteNamespace(networkPolicy.Namespace))
if err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("Error deleting resource %s: %v", networkPolicy.String(), err)
}
}
return err
}

View File

@@ -45,6 +45,20 @@ rules:
- get - get
- watch - watch
- list - list
- apiGroups:
- "networking.k8s.io"
resources:
- networkpolicy
- networkpolicies
verbs:
- get
- create
- update
- delete
- deletecollection
- list
- patch
- watch
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding

View File

@@ -81,7 +81,7 @@ func serviceKey(s *runtime.Service) string {
} }
// Create creates a new service which is then started by runtime // Create creates a new service which is then started by runtime
func (r *localRuntime) Create(s *runtime.Service, opts ...runtime.CreateOption) error { func (r *localRuntime) Create(resource runtime.Resource, opts ...runtime.CreateOption) error {
var options runtime.CreateOptions var options runtime.CreateOptions
for _, o := range opts { for _, o := range opts {
o(&options) o(&options)
@@ -90,55 +90,74 @@ func (r *localRuntime) Create(s *runtime.Service, opts ...runtime.CreateOption)
r.Lock() r.Lock()
defer r.Unlock() defer r.Unlock()
if len(options.Namespace) == 0 { // Handle the various different types of resources:
options.Namespace = defaultNamespace switch resource.Type() {
} case runtime.TypeNamespace:
if len(options.Entrypoint) > 0 { // noop (Namespace is not supported by local)
s.Source = filepath.Join(s.Source, options.Entrypoint) return nil
} case runtime.TypeNetworkPolicy:
if len(options.Command) == 0 { // noop (NetworkPolicy is not supported by local)
ep, err := Entrypoint(s.Source) return nil
if err != nil { case runtime.TypeService:
return err
// Assert the resource back into a *runtime.Service
s, ok := resource.(*runtime.Service)
if !ok {
return runtime.ErrInvalidResource
} }
options.Command = []string{"go"} if len(options.Namespace) == 0 {
options.Args = []string{"run", ep} options.Namespace = defaultNamespace
} }
if len(options.Entrypoint) > 0 {
s.Source = filepath.Join(s.Source, options.Entrypoint)
}
if len(options.Command) == 0 {
ep, err := Entrypoint(s.Source)
if err != nil {
return err
}
// pass secrets as env vars options.Command = []string{"go"}
for key, value := range options.Secrets { options.Args = []string{"run", ep}
options.Env = append(options.Env, fmt.Sprintf("%v=%v", key, value)) }
}
if _, ok := r.namespaces[options.Namespace]; !ok { // pass secrets as env vars
r.namespaces[options.Namespace] = make(map[string]*service) for key, value := range options.Secrets {
} options.Env = append(options.Env, fmt.Sprintf("%v=%v", key, value))
if _, ok := r.namespaces[options.Namespace][serviceKey(s)]; ok { }
return runtime.ErrAlreadyExists
}
// create new service if _, ok := r.namespaces[options.Namespace]; !ok {
service := newService(s, options) r.namespaces[options.Namespace] = make(map[string]*service)
}
if _, ok := r.namespaces[options.Namespace][serviceKey(s)]; ok {
return runtime.ErrAlreadyExists
}
f, err := os.OpenFile(logFile(service.Name), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) // create new service
if err != nil { service := newService(s, options)
log.Fatal(err)
}
if service.output != nil { f, err := os.OpenFile(logFile(service.Name), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
service.output = io.MultiWriter(service.output, f) if err != nil {
} else { log.Fatal(err)
service.output = f }
}
// start the service
if err := service.Start(); err != nil {
return err
}
// save service
r.namespaces[options.Namespace][serviceKey(s)] = service
return nil if service.output != nil {
service.output = io.MultiWriter(service.output, f)
} else {
service.output = f
}
// start the service
if err := service.Start(); err != nil {
return err
}
// save service
r.namespaces[options.Namespace][serviceKey(s)] = service
return nil
default:
return runtime.ErrInvalidResource
}
} }
// exists returns whether the given file or directory exists // exists returns whether the given file or directory exists
@@ -157,65 +176,85 @@ func exists(path string) (bool, error) {
// The reason for this is because it's hard to calculate line offset // The reason for this is because it's hard to calculate line offset
// as opposed to character offset. // as opposed to character offset.
// This logger streams by default and only supports the `StreamCount` option. // This logger streams by default and only supports the `StreamCount` option.
func (r *localRuntime) Logs(s *runtime.Service, options ...runtime.LogsOption) (runtime.Logs, error) { func (r *localRuntime) Logs(resource runtime.Resource, options ...runtime.LogsOption) (runtime.Logs, error) {
lopts := runtime.LogsOptions{} lopts := runtime.LogsOptions{}
for _, o := range options { for _, o := range options {
o(&lopts) o(&lopts)
} }
ret := &logStream{
service: s.Name,
stream: make(chan runtime.Log),
stop: make(chan bool),
}
fpath := logFile(s.Name) // Handle the various different types of resources:
if ex, err := exists(fpath); err != nil { switch resource.Type() {
return nil, err case runtime.TypeNamespace:
} else if !ex { // noop (Namespace is not supported by local)
return nil, fmt.Errorf("Logs not found for service %s", s.Name) return nil, nil
} case runtime.TypeNetworkPolicy:
// noop (NetworkPolicy is not supported by local)
return nil, nil
case runtime.TypeService:
// have to check file size to avoid too big of a seek // Assert the resource back into a *runtime.Service
fi, err := os.Stat(fpath) s, ok := resource.(*runtime.Service)
if err != nil { if !ok {
return nil, err return nil, runtime.ErrInvalidResource
}
size := fi.Size()
whence := 2
// Multiply by length of an average line of log in bytes
offset := lopts.Count * 200
if offset > size {
offset = size
}
offset *= -1
t, err := tail.TailFile(fpath, tail.Config{Follow: lopts.Stream, Location: &tail.SeekInfo{
Whence: whence,
Offset: int64(offset),
}, Logger: tail.DiscardingLogger})
if err != nil {
return nil, err
}
ret.tail = t
go func() {
for {
select {
case line, ok := <-t.Lines:
if !ok {
ret.Stop()
return
}
ret.stream <- runtime.Log{Message: line.Text}
case <-ret.stop:
return
}
} }
}() ret := &logStream{
return ret, nil service: s.Name,
stream: make(chan runtime.Log),
stop: make(chan bool),
}
fpath := logFile(s.Name)
if ex, err := exists(fpath); err != nil {
return nil, err
} else if !ex {
return nil, fmt.Errorf("Logs not found for service %s", s.Name)
}
// have to check file size to avoid too big of a seek
fi, err := os.Stat(fpath)
if err != nil {
return nil, err
}
size := fi.Size()
whence := 2
// Multiply by length of an average line of log in bytes
offset := lopts.Count * 200
if offset > size {
offset = size
}
offset *= -1
t, err := tail.TailFile(fpath, tail.Config{Follow: lopts.Stream, Location: &tail.SeekInfo{
Whence: whence,
Offset: int64(offset),
}, Logger: tail.DiscardingLogger})
if err != nil {
return nil, err
}
ret.tail = t
go func() {
for {
select {
case line, ok := <-t.Lines:
if !ok {
ret.Stop()
return
}
ret.stream <- runtime.Log{Message: line.Text}
case <-ret.stop:
return
}
}
}()
return ret, nil
default:
return nil, runtime.ErrInvalidResource
}
} }
type logStream struct { type logStream struct {
@@ -298,84 +337,126 @@ func (r *localRuntime) Read(opts ...runtime.ReadOption) ([]*runtime.Service, err
} }
// Update attempts to update the service // Update attempts to update the service
func (r *localRuntime) Update(s *runtime.Service, opts ...runtime.UpdateOption) error { func (r *localRuntime) Update(resource runtime.Resource, opts ...runtime.UpdateOption) error {
var options runtime.UpdateOptions var options runtime.UpdateOptions
for _, o := range opts { for _, o := range opts {
o(&options) o(&options)
} }
if len(options.Namespace) == 0 {
options.Namespace = defaultNamespace
}
if len(options.Entrypoint) > 0 {
s.Source = filepath.Join(s.Source, options.Entrypoint)
}
r.Lock() // Handle the various different types of resources:
srvs, ok := r.namespaces[options.Namespace] switch resource.Type() {
r.Unlock() case runtime.TypeNamespace:
if !ok { // noop (Namespace is not supported by local)
return errors.New("Service not found") return nil
} case runtime.TypeNetworkPolicy:
// noop (NetworkPolicy is not supported by local)
return nil
case runtime.TypeService:
r.Lock() // Assert the resource back into a *runtime.Service
service, ok := srvs[serviceKey(s)] s, ok := resource.(*runtime.Service)
r.Unlock() if !ok {
if !ok { return runtime.ErrInvalidResource
return errors.New("Service not found") }
}
if err := service.Stop(); err != nil && err.Error() != "no such process" { if len(options.Entrypoint) > 0 {
logger.Errorf("Error stopping service %s: %s", service.Name, err) s.Source = filepath.Join(s.Source, options.Entrypoint)
return err }
}
// update the source to the new location and restart the service if len(options.Namespace) == 0 {
service.Source = s.Source options.Namespace = defaultNamespace
service.Exec.Dir = s.Source }
return service.Start()
r.Lock()
srvs, ok := r.namespaces[options.Namespace]
r.Unlock()
if !ok {
return errors.New("Service not found")
}
r.Lock()
service, ok := srvs[serviceKey(s)]
r.Unlock()
if !ok {
return errors.New("Service not found")
}
if err := service.Stop(); err != nil && err.Error() != "no such process" {
logger.Errorf("Error stopping service %s: %s", service.Name, err)
return err
}
// update the source to the new location and restart the service
service.Source = s.Source
service.Exec.Dir = s.Source
return service.Start()
default:
return runtime.ErrInvalidResource
}
} }
// Delete removes the service from the runtime and stops it // Delete removes the service from the runtime and stops it
func (r *localRuntime) Delete(s *runtime.Service, opts ...runtime.DeleteOption) error { func (r *localRuntime) Delete(resource runtime.Resource, opts ...runtime.DeleteOption) error {
r.Lock()
defer r.Unlock()
var options runtime.DeleteOptions // Handle the various different types of resources:
for _, o := range opts { switch resource.Type() {
o(&options) case runtime.TypeNamespace:
} // noop (Namespace is not supported by local)
if len(options.Namespace) == 0 {
options.Namespace = defaultNamespace
}
srvs, ok := r.namespaces[options.Namespace]
if !ok {
return nil return nil
} case runtime.TypeNetworkPolicy:
// noop (NetworkPolicy is not supported by local)
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
logger.Debugf("Runtime deleting service %s", s.Name)
}
service, ok := srvs[serviceKey(s)]
if !ok {
return nil return nil
} case runtime.TypeService:
// check if running // Assert the resource back into a *runtime.Service
if !service.Running() { s, ok := resource.(*runtime.Service)
if !ok {
return runtime.ErrInvalidResource
}
r.Lock()
defer r.Unlock()
var options runtime.DeleteOptions
for _, o := range opts {
o(&options)
}
if len(options.Namespace) == 0 {
options.Namespace = defaultNamespace
}
srvs, ok := r.namespaces[options.Namespace]
if !ok {
return nil
}
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
logger.Debugf("Runtime deleting service %s", s.Name)
}
service, ok := srvs[serviceKey(s)]
if !ok {
return nil
}
// check if running
if !service.Running() {
delete(srvs, service.key())
r.namespaces[options.Namespace] = srvs
return nil
}
// otherwise stop it
if err := service.Stop(); err != nil {
return err
}
// delete it
delete(srvs, service.key()) delete(srvs, service.key())
r.namespaces[options.Namespace] = srvs r.namespaces[options.Namespace] = srvs
return nil return nil
default:
return runtime.ErrInvalidResource
} }
// otherwise stop it
if err := service.Stop(); err != nil {
return err
}
// delete it
delete(srvs, service.key())
r.namespaces[options.Namespace] = srvs
return nil
} }
// Start starts the runtime // Start starts the runtime
@@ -461,13 +542,3 @@ func Entrypoint(dir string) (string, error) {
return "", errors.New("More than one entrypoint found") return "", errors.New("More than one entrypoint found")
} }
} }
func (r *localRuntime) CreateNamespace(ns string) error {
// noop
return nil
}
func (r *localRuntime) DeleteNamespace(ns string) error {
// noop
return nil
}

117
runtime/resource.go Normal file
View File

@@ -0,0 +1,117 @@
// Package runtime is a service runtime manager
package runtime
import "fmt"
const (
TypeNamespace = "namespace"
TypeNetworkPolicy = "networkpolicy"
TypeService = "service"
)
// Resource represents any resource handled by runtime
type Resource interface {
String() string
Type() string
}
// Namespace represents a logical namespace for organising resources
type Namespace struct {
// Name of the namespace
Name string
}
// NewNamespace mints a new namespace
func NewNamespace(name string) (*Namespace, error) {
if name == "" {
return nil, ErrInvalidResource
}
return &Namespace{
Name: name,
}, nil
}
// String implements Resource
func (r *Namespace) String() string {
return r.Name
}
// Type implements Resource
func (*Namespace) Type() string {
return TypeNamespace
}
// NetworkPolicy represents an ACL of label pairs allowing ignress to a namespace
type NetworkPolicy struct {
// The labels allowed ingress by this policy
AllowedLabels map[string]string
// Name of the network policy
Name string
// Namespace the network policy belongs to
Namespace string
}
// NewNetworkPolicy mints a new networkpolicy
func NewNetworkPolicy(name, namespace string, allowedLabels map[string]string) (*NetworkPolicy, error) {
if name == "" || namespace == "" {
return nil, ErrInvalidResource
}
if allowedLabels == nil {
allowedLabels = map[string]string{
"origin": "micro",
}
}
return &NetworkPolicy{
AllowedLabels: allowedLabels,
Name: name,
Namespace: namespace,
}, nil
}
// String implements Resource
func (r *NetworkPolicy) String() string {
return fmt.Sprintf("%s.%s", r.Namespace, r.Name)
}
// Type implements Resource
func (*NetworkPolicy) Type() string {
return TypeNetworkPolicy
}
// Service represents a Micro service running within a namespace
type Service struct {
// Name of the service
Name string
// Version of the service
Version string
// url location of source
Source string
// Metadata stores metadata
Metadata map[string]string
// Status of the service
Status ServiceStatus
}
// NewService mints a new service
func NewService(name, version string) (*Service, error) {
if name == "" {
return nil, ErrInvalidResource
}
if version == "" {
version = "latest"
}
return &Service{
Name: name,
Version: version,
}, nil
}
// String implements Resource
func (r *Service) String() string {
return fmt.Sprintf("service://%s@%s:%s", r.Metadata["namespace"], r.Name, r.Version)
}
// Type implements Resource
func (*Service) Type() string {
return TypeService
}

65
runtime/resource_test.go Normal file
View File

@@ -0,0 +1,65 @@
package runtime
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestResources(t *testing.T) {
// Namespace:
assert.Equal(t, TypeNamespace, new(Namespace).Type())
namespace, err := NewNamespace("")
assert.Error(t, err)
assert.Equal(t, ErrInvalidResource, err)
assert.Nil(t, namespace)
namespace, err = NewNamespace("test-namespace")
assert.NoError(t, err)
assert.NotNil(t, namespace)
assert.Equal(t, TypeNamespace, namespace.Type())
assert.Equal(t, "test-namespace", namespace.String())
// NetworkPolicy:
assert.Equal(t, TypeNetworkPolicy, new(NetworkPolicy).Type())
networkPolicy, err := NewNetworkPolicy("", "", nil)
assert.Error(t, err)
assert.Equal(t, ErrInvalidResource, err)
assert.Nil(t, networkPolicy)
networkPolicy, err = NewNetworkPolicy("test", "", nil)
assert.Error(t, err)
assert.Equal(t, ErrInvalidResource, err)
assert.Nil(t, networkPolicy)
networkPolicy, err = NewNetworkPolicy("", "test", nil)
assert.Error(t, err)
assert.Equal(t, ErrInvalidResource, err)
assert.Nil(t, networkPolicy)
networkPolicy, err = NewNetworkPolicy("ingress", "test", nil)
assert.NoError(t, err)
assert.NotNil(t, networkPolicy)
assert.Equal(t, TypeNetworkPolicy, networkPolicy.Type())
assert.Equal(t, "test.ingress", networkPolicy.String())
assert.Len(t, networkPolicy.AllowedLabels, 1)
networkPolicy, err = NewNetworkPolicy("ingress", "test", map[string]string{"foo": "bar", "bar": "foo"})
assert.Len(t, networkPolicy.AllowedLabels, 2)
// Service:
assert.Equal(t, TypeService, new(Service).Type())
service, err := NewService("", "")
assert.Error(t, err)
assert.Equal(t, ErrInvalidResource, err)
assert.Nil(t, service)
service, err = NewService("test-service", "oldest")
service.Metadata = map[string]string{"namespace": "testing"}
assert.NoError(t, err)
assert.NotNil(t, service)
assert.Equal(t, TypeService, service.Type())
assert.Equal(t, "service://testing@test-service:oldest", service.String())
assert.Equal(t, "oldest", service.Version)
}

View File

@@ -7,28 +7,25 @@ import (
) )
var ( var (
ErrAlreadyExists = errors.New("already exists") ErrAlreadyExists = errors.New("already exists")
ErrNotFound = errors.New("not found") ErrInvalidResource = errors.New("invalid resource")
ErrNotFound = errors.New("not found")
) )
// Runtime is a service runtime manager // Runtime is a service runtime manager
type Runtime interface { type Runtime interface {
// Init initializes runtime // Init initializes runtime
Init(...Option) error Init(...Option) error
// CreateNamespace creates a new namespace in the runtime // Create a resource
CreateNamespace(string) error Create(Resource, ...CreateOption) error
// DeleteNamespace deletes a namespace in the runtime // Read a resource
DeleteNamespace(string) error
// Create registers a service
Create(*Service, ...CreateOption) error
// Read returns the service
Read(...ReadOption) ([]*Service, error) Read(...ReadOption) ([]*Service, error)
// Update the service in place // Update a resource
Update(*Service, ...UpdateOption) error Update(Resource, ...UpdateOption) error
// Remove a service // Delete a resource
Delete(*Service, ...DeleteOption) error Delete(Resource, ...DeleteOption) error
// Logs returns the logs for a service // Logs returns the logs for a resource
Logs(*Service, ...LogsOption) (Logs, error) Logs(Resource, ...LogsOption) (Logs, error)
// Start starts the runtime // Start starts the runtime
Start() error Start() error
// Stop shuts down the runtime // Stop shuts down the runtime
@@ -113,20 +110,6 @@ const (
Error Error
) )
// Service is runtime service
type Service struct {
// Name of the service
Name string
// Version of the service
Version string
// url location of source
Source string
// Metadata stores metadata
Metadata map[string]string
// Status of the service
Status ServiceStatus
}
// Resources which are allocated to a serivce // Resources which are allocated to a serivce
type Resources struct { type Resources struct {
// CPU is the maximum amount of CPU the service will be allocated (unit millicpu) // CPU is the maximum amount of CPU the service will be allocated (unit millicpu)

View File

@@ -168,6 +168,9 @@ func (r *Request) request() (*http.Request, error) {
case "deployment": case "deployment":
// /apis/apps/v1/namespaces/{namespace}/deployments/{name} // /apis/apps/v1/namespaces/{namespace}/deployments/{name}
url = fmt.Sprintf("%s/apis/apps/v1/namespaces/%s/%ss/", r.host, r.namespace, r.resource) url = fmt.Sprintf("%s/apis/apps/v1/namespaces/%s/%ss/", r.host, r.namespace, r.resource)
case "networkpolicy", "networkpolicies":
// /apis/networking.k8s.io/v1/namespaces/{namespace}/networkpolicies
url = fmt.Sprintf("%s/apis/networking.k8s.io/v1/namespaces/%s/networkpolicies/", r.host, r.namespace)
default: default:
// /api/v1/namespaces/{namespace}/{resource} // /api/v1/namespaces/{namespace}/{resource}
url = fmt.Sprintf("%s/api/v1/namespaces/%s/%ss/", r.host, r.namespace, r.resource) url = fmt.Sprintf("%s/api/v1/namespaces/%s/%ss/", r.host, r.namespace, r.resource)

View File

@@ -156,6 +156,8 @@ func (c *client) Update(r *Resource, opts ...UpdateOption) error {
req.Body(r.Value.(*Deployment)) req.Body(r.Value.(*Deployment))
case "pod": case "pod":
req.Body(r.Value.(*Pod)) req.Body(r.Value.(*Pod))
case "networkpolicy", "networkpolicies":
req.Body(r.Value.(*NetworkPolicy))
default: default:
return errors.New("unsupported resource") return errors.New("unsupported resource")
} }

View File

@@ -1,11 +1,13 @@
package client package client
var templates = map[string]string{ var templates = map[string]string{
"deployment": deploymentTmpl, "deployment": deploymentTmpl,
"service": serviceTmpl, "service": serviceTmpl,
"namespace": namespaceTmpl, "namespace": namespaceTmpl,
"secret": secretTmpl, "secret": secretTmpl,
"serviceaccount": serviceAccountTmpl, "serviceaccount": serviceAccountTmpl,
"networkpolicies": networkPolicyTmpl,
"networkpolicy": networkPolicyTmpl,
} }
var deploymentTmpl = ` var deploymentTmpl = `
@@ -239,3 +241,31 @@ imagePullSecrets:
{{- end }} {{- end }}
{{- end }} {{- end }}
` `
var networkPolicyTmpl = `
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: "{{ .Metadata.Name }}"
namespace: "{{ .Metadata.Namespace }}"
labels:
{{- with .Metadata.Labels }}
{{- range $key, $value := . }}
{{ $key }}: "{{ $value }}"
{{- end }}
{{- end }}
spec:
podSelector:
matchLabels:
ingress:
- from: # Allow pods in this namespace to talk to each other
- podSelector: {}
- from: # Allow pods in the namespaces bearing the specified labels to talk to pods in this namespace:
- namespaceSelector:
matchLabels:
{{- with .AllowedLabels }}
{{- range $key, $value := . }}
{{ $key }}: "{{ $value }}"
{{- end }}
{{- end }}
`

View File

@@ -267,3 +267,9 @@ type VolumeMount struct {
Name string `json:"name"` Name string `json:"name"`
MountPath string `json:"mountPath"` MountPath string `json:"mountPath"`
} }
// NetworkPolicy is a Kubernetes Namespace
type NetworkPolicy struct {
AllowedLabels map[string]string `json:"allowedLabels,omitempty"`
Metadata *Metadata `json:"metadata,omitempty"`
}