Unverified Commit ac4bbfee authored by Caleb Bron's avatar Caleb Bron Committed by GitHub
Browse files

Merge pull request #722 from cbron/stack-export

Enable export of stack by gvk
parents b97101cf 8fb5abfa
Showing with 271 additions and 81 deletions
+271 -81
......@@ -10,10 +10,12 @@ import (
riov1 "github.com/rancher/rio/pkg/apis/rio.cattle.io/v1"
"github.com/rancher/rio/pkg/riofile"
"github.com/rancher/wrangler/pkg/gvk"
name "github.com/rancher/wrangler/pkg/name"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/yaml"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
)
type Export struct {
......@@ -46,7 +48,15 @@ func (e *Export) Run(ctx *clicontext.CLIContext) error {
if err != nil {
return err
}
if err := exportObjects(objects, output, e.Riofile, e.Format); err != nil {
return err
}
case clitypes.StackType:
stack := r.Object.(*riov1.Stack)
objects, err := collectObjectsFromStack(ctx, stack)
if err != nil {
return err
}
if err := exportObjects(objects, output, e.Riofile, e.Format); err != nil {
return err
}
......@@ -82,6 +92,74 @@ func (e *Export) Run(ctx *clicontext.CLIContext) error {
return nil
}
func collectObjectsFromStack(ctx *clicontext.CLIContext, stack *riov1.Stack) ([]runtime.Object, error) {
var objects []runtime.Object
client, err := dynamic.NewForConfig(ctx.Config.RestConfig)
if err != nil {
return nil, err
}
for _, gvk := range stack.Spec.AdditionalGroupVersionKinds {
gvr := schema.GroupVersionResource{
Group: gvk.Group,
Version: gvk.Version,
Resource: strings.ToLower(name.GuessPluralName(gvk.Kind)),
}
resourceClient := client.Resource(gvr)
ul, err := resourceClient.Namespace(stack.Namespace).List(metav1.ListOptions{
LabelSelector: "rio.cattle.io/stack=" + stack.Name,
})
if err != nil {
return nil, err
}
for _, item := range ul.Items {
if item.GroupVersionKind().Group == "rio.cattle.io" {
data, err := item.MarshalJSON()
if err != nil {
return nil, err
}
if item.GetKind() == "Service" {
svc := riov1.Service{}
if err := json.Unmarshal(data, &svc); err != nil {
return nil, err
}
objects = append(objects, &svc)
} else if item.GetKind() == "ExternalService" {
es := &riov1.ExternalService{}
if err := json.Unmarshal(data, es); err != nil {
return nil, err
}
objects = append(objects, es)
} else if item.GetKind() == "ConfigMap" {
cm := &corev1.ConfigMap{}
if err := json.Unmarshal(data, cm); err != nil {
return nil, err
}
objects = append(objects, cm)
} else if item.GetKind() == "Router" {
rt := &riov1.Router{}
if err := json.Unmarshal(data, rt); err != nil {
return nil, err
}
objects = append(objects, rt)
}
} else {
// From here down we only want to export non-rio created objects which would already be exported above
stackObj := true
for k, v := range item.GetAnnotations() {
if stackObj == true && k == "objectset.rio.cattle.io/owner-gvk" && v != "rio.cattle.io/v1, Kind=Stack" {
stackObj = false
break
}
}
if stackObj == true {
objects = append(objects, item.DeepCopyObject())
}
}
}
}
return objects, nil
}
func collectObjectsFromNs(ctx *clicontext.CLIContext, ns string) ([]runtime.Object, error) {
var objects []runtime.Object
......@@ -136,8 +214,8 @@ func configmapsFromService(ctx *clicontext.CLIContext, svc riov1.Service) ([]run
return objects, nil
}
func exportObjects(objects []runtime.Object, output *strings.Builder, riofile bool, format string) error {
if !riofile {
func exportObjects(objects []runtime.Object, output *strings.Builder, riofileFormat bool, format string) error {
if !riofileFormat {
return exportObjectsNative(objects, output, format)
}
......@@ -156,7 +234,7 @@ func exportObjectStack(objects []runtime.Object, output *strings.Builder) error
func exportObjectsNative(objects []runtime.Object, output *strings.Builder, format string) error {
for _, obj := range objects {
result, err := objToYaml(obj, format)
result, err := riofile.ObjToYaml(obj, format)
if err != nil {
return err
}
......@@ -168,21 +246,3 @@ func exportObjectsNative(objects []runtime.Object, output *strings.Builder, form
}
return nil
}
func objToYaml(obj runtime.Object, format string) (string, error) {
data, err := json.MarshalIndent(obj, "", " ")
if err != nil {
return "", err
}
if format == "json" {
return string(data), nil
}
r, err := yaml.JSONToYAML(data)
if err != nil {
return "", err
}
return string(r), nil
}
......@@ -2,6 +2,7 @@ package riofile
import (
"bytes"
"encoding/json"
"strings"
"github.com/rancher/norman/pkg/types/convert"
......@@ -14,6 +15,7 @@ import (
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
k8yaml "sigs.k8s.io/yaml"
)
type Riofile struct {
......@@ -93,12 +95,17 @@ func Render(objects []runtime.Object) ([]byte, error) {
}
if len(other) > 0 {
bytes, err := yaml.ToBytes(other)
if err != nil {
return nil, err
var otherContent string
for _, o := range other {
str, err := ObjToYaml(o, "yaml")
if err != nil {
return nil, err
}
otherContent += str
otherContent += "---\n"
}
rf.Kubernetes = &schema.Kubernetes{
Manifest: string(bytes),
Manifest: otherContent,
}
}
......@@ -277,3 +284,69 @@ func toRiofile(rf *schema.ExternalRiofile) (*Riofile, error) {
return riofile, nil
}
func ObjToYaml(obj runtime.Object, format string) (string, error) {
data, err := json.MarshalIndent(obj, "", " ")
if err != nil {
return "", err
}
m := make(map[string]interface{})
if err := json.Unmarshal(data, &m); err != nil {
return "", err
}
modifiedMap := make(map[string]interface{})
newMeta := map[string]interface{}{}
if meta, ok := m["metadata"].(map[string]interface{}); ok {
if meta["labels"] != nil {
labels := cleanLabels(meta["labels"].(map[string]interface{}))
if len(labels) != 0 {
newMeta["labels"] = labels
}
}
if meta["annotations"] != nil {
anno := cleanLabels(meta["annotations"].(map[string]interface{}))
if len(anno) != 0 {
newMeta["annotations"] = anno
}
}
newMeta["name"] = meta["name"]
newMeta["namespace"] = meta["namespace"]
}
modifiedMap["apiVersion"] = m["apiVersion"]
modifiedMap["kind"] = m["kind"]
modifiedMap["metadata"] = newMeta
if _, ok := m["spec"]; ok {
modifiedMap["spec"] = m["spec"]
}
if m["data"] != nil {
modifiedMap["data"] = m["data"]
}
data, err = json.MarshalIndent(modifiedMap, "", " ")
if err != nil {
return "", err
}
if format == "json" {
return string(data), nil
}
r, err := k8yaml.JSONToYAML(data)
if err != nil {
return "", err
}
return string(r), nil
}
func cleanLabels(m map[string]interface{}) map[string]interface{} {
r := make(map[string]interface{})
for k, v := range m {
if !strings.Contains(k, "rio.cattle.io") {
r[k] = v
}
}
return r
}
externalservices:
es-foo:
fqdn: www.example.com
es-bar:
ipAddresses:
- 1.1.1.1
- 1.1.1.1
es-foo:
fqdn: www.example.com
routers:
route-foo:
routes:
- match:
path:
exact: /v0
to:
- version: v0
app: export-test-image
- match:
path:
exact: /v3
to:
- version: v3
app: export-test-image
- match:
path:
exact: /v0
to:
- app: export-test-image
version: v0
- match:
path:
exact: /v3
to:
- app: export-test-image
version: v3
services:
export-test-image:
app: export-test-image
version: v0
rolloutConfig:
increment: 5
interval: 5
autoscale:
concurrency: 10
maxReplicas: 10
minReplicas: 1
image: ibuildthecloud/demo:v1
imagePullPolicy: IfNotPresent
ports:
- "80"
scale: 1
autoscale:
concurrency: 10
min: 1
max: 10
- "80"
replicas: 1
version: v0
weight: 100
export-test-image-v3:
app: export-test-image
version: v3
autoscale:
concurrency: 10
maxReplicas: 10
minReplicas: 1
image: ibuildthecloud/demo:v3
imagePullPolicy: IfNotPresent
ports:
- "80"
rolloutConfig:
increment: 5
interval: 5
autoscale:
concurrency: 10
min: 1
max: 10
scale: 1
replicas: 1
version: v3
weight: 100
kubernetes:
manifest: |-
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
tier: backend
role: master
spec:
ports:
- port: 80
targetPort: 80
selector:
app: nginx
tier: backend
role: master
---
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2 # tells deployment to run 2 pods matching the template
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
......@@ -2,6 +2,7 @@ package integration
import (
"fmt"
"reflect"
"testing"
riov1 "github.com/rancher/rio/pkg/apis/rio.cattle.io/v1"
......@@ -26,15 +27,27 @@ func riofileTests(t *testing.T, when spec.G, it spec.S) {
it("should correctly build the system", func() {
// Export check
// todo: fix during https://github.com/rancher/rio/issues/641
//assert.Equal(t, strings.Trim(riofile.Readfile(), "\n"), strings.Trim(riofile.ExportStack(), "\n"), "should have stack export be same as original file")
// external services
export, err := riofile.ExportStack()
assert.Nil(t, err)
orig, err := riofile.Readfile()
assert.Nil(t, err)
assert.True(t, reflect.DeepEqual(orig["services"], export["services"]), "export should have same services as file")
assert.True(t, reflect.DeepEqual(orig["externalservices"], export["externalservices"]), "export should have same externalservices as file")
assert.True(t, reflect.DeepEqual(orig["routers"], export["routers"]), "export should have same routers as file")
assert.Contains(t, export, "kubernetes")
// external service FQDN
externalFoo := testutil.GetExternalService(t, "es-foo")
assert.Equal(t, "www.example.com", externalFoo.GetFQDN(), "should have external service with fqdn")
assert.Equal(t, externalFoo.GetKubeFQDN(), externalFoo.GetFQDN(), "should have external service with fqdn from k8s")
externalFQDN := externalFoo.GetFQDN()
assert.Equal(t, "www.example.com", externalFQDN, "should have external service with fqdn")
assert.Equal(t, externalFQDN, externalFoo.GetKubeFQDN(), "should have external service with fqdn from k8s")
// external service IP
externalBar := testutil.GetExternalService(t, "es-bar")
assert.Equal(t, "1.1.1.1", externalBar.GetFirstIPAddress(), "should have external service with ip")
assert.Equal(t, externalBar.GetFirstIPAddress(), externalBar.GetKubeFirstIPAddress(), "should have external service with ip from k8s")
externalIP := externalBar.GetFirstIPAddress()
assert.Equal(t, "1.1.1.1", externalIP, "should have external service with ip")
assert.Equal(t, externalIP, externalBar.GetKubeFirstIPAddress(), "should have external service with ip from k8s")
// services and their endpoints
serviceV0 := testutil.GetService(t, "export-test-image", "export-test-image", "v0")
serviceV3 := testutil.GetService(t, "export-test-image", "export-test-image", "v3")
......@@ -46,6 +59,7 @@ func riofileTests(t *testing.T, when spec.G, it spec.S) {
assert.Contains(t, pod, serviceV0.App)
assert.Contains(t, pod, "2/2")
}
// routers and their endpoints
routerFooV0 := testutil.GetRoute(t, "route-foo", "/v0")
routerFooV3 := testutil.GetRoute(t, "route-foo", "/v3")
......@@ -69,10 +83,12 @@ func riofileTests(t *testing.T, when spec.G, it spec.S) {
riofile.Remove()
})
it("should bringup the k8s sample app", func() {
// Export check
// todo: fix during https://github.com/rancher/rio/issues/641
//assert.Equal(t, strings.Trim(riofile.Readfile(), "\n"), strings.Trim(riofile.ExportStack(), "\n"), "should have stack export be same as original file")
it("should bring up the k8s sample app", func() {
// Export check, ensure manifest services end up as rio services
export, err := riofile.ExportStack()
assert.Nil(t, err)
exportSvcs := export["services"].(map[string]interface{})
assert.Contains(t, exportSvcs, "nginx")
// check frontend service came up
frontendSvc := testutil.TestService{
......@@ -93,7 +109,7 @@ func riofileTests(t *testing.T, when spec.G, it spec.S) {
defer riofile.Remove()
//check that services/pods/deployments are removed
_, err := testutil.WaitForNoResponse(fmt.Sprintf("%s%s", frontendRoute.Router.Status.Endpoints[0], frontendRoute.Path))
_, err = testutil.WaitForNoResponse(fmt.Sprintf("%s%s", frontendRoute.Router.Status.Endpoints[0], frontendRoute.Path))
assert.Nil(t, err, "the endpoint should go down")
_, err = testutil.KubectlCmd([]string{"get", "-n", testutil.TestingNamespace, "svc", "frontend"})
assert.Error(t, err, "kubectl should return an error since the service is being removed")
......
......@@ -9,6 +9,7 @@ import (
"testing"
riov1 "github.com/rancher/rio/pkg/apis/rio.cattle.io/v1"
"github.com/rancher/wrangler/pkg/yaml"
)
type TestRiofile struct {
......@@ -76,22 +77,30 @@ func (trf *TestRiofile) Remove() {
}
}
// Return "rio export --stack {name}" as string
func (trf *TestRiofile) ExportStack() string {
out, err := RioCmd([]string{"export", "--riofile", trf.Name})
// Return "rio export --stack {name}"
func (trf *TestRiofile) ExportStack() (map[string]interface{}, error) {
content, err := RioCmd([]string{"export", "--riofile", trf.Name})
if err != nil {
trf.T.Log(err.Error())
}
return strings.TrimSuffix(out, "\n")
data := map[string]interface{}{}
if err := yaml.Unmarshal([]byte(content), &data); err != nil {
return nil, err
}
return data, nil
}
// Returns raw Riofile as string
func (trf *TestRiofile) Readfile() string {
contents, err := ioutil.ReadFile(trf.Filepath)
// Returns raw Riofile
func (trf *TestRiofile) Readfile() (map[string]interface{}, error) {
content, err := ioutil.ReadFile(trf.Filepath)
if err != nil {
trf.T.Log(err.Error())
}
return strings.TrimSuffix(string(contents), "\n")
data := map[string]interface{}{}
if err := yaml.Unmarshal(content, &data); err != nil {
return nil, err
}
return data, nil
}
//////////////////
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment