Repository: kubernetes/kompose
Pull request #1452 (2021-10-23 13:00:18)

Add initial support for statefulset controller.
This PR adds option to use `StatefulSet` as a controller.
Usage: 
```bash
kompose convert -f ./path/to/docker-compose.yml  --controller statefulset
```

Example 

```yaml
# docker-compose.yaml
version: '2'
services:
  web:
    image: python
    volumes:
     - app:/app
    ports:
     - "8080:8080"

volumes:
  app:
```

Kompose output 
```yaml
apiVersion: v1
items:
  - apiVersion: v1
    kind: Service
    metadata:
      annotations:
        kompose.cmd: ./kompose convert -f ./compose-test.yaml --stdout --controller statefulset
        kompose.version: 1.24.0 (4a2a0458)
      creationTimestamp: null
      labels:
        io.kompose.service: web
      name: web
    spec:
      clusterIP: None
      ports:
        - name: "8080"
          port: 8080
          targetPort: 8080
      selector:
        io.kompose.service: web
      type: ClusterIP
    status:
      loadBalancer: {}
  - apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      annotations:
        kompose.cmd: ./kompose convert -f ./compose-test.yaml --stdout --controller statefulset
        kompose.version: 1.24.0 (4a2a0458)
      creationTimestamp: null
      labels:
        io.kompose.service: web
      name: web
    spec:
      replicas: 1
      selector:
        matchLabels:
          io.kompose.service: web
      serviceName: web
      template:
        metadata:
          creationTimestamp: null
          labels:
            io.kompose.service: web
        spec:
          containers:
            - image: python
              name: web
              ports:
                - containerPort: 8080
              resources: {}
              volumeMounts:
                - mountPath: /app
                  name: app
          restartPolicy: Always
      updateStrategy: {}
      volumeClaimTemplates:
        - metadata:
            creationTimestamp: null
            labels:
              io.kompose.service: app
            name: app
          spec:
            accessModes:
              - ReadWriteOnce
            resources:
              requests:
                storage: 100Mi
          status: {}
    status:
      replicas: 0
kind: List
metadata: {}

```

TODOs:

- [ ] Get the statefulset option right
- [x] Add cmd tests
- [x] Add unit tests 
- [x] Add headless service for netwoking.



hangyan: I have looked through the code. Looks good for now, maybe i will double check later. And we still need some unittest and e2e test. you can check up the github actions for some clues.
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/kompose/pull/1452#pullrequestreview-796731327" title="Approved">hangyan</a>*, *<a href="https://github.com/kubernetes/kompose/pull/1452#" title="Author self-approved">sbs2001</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fkompose).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes/kompose/blob/master/OWNERS)~~ [hangyan]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
hangyan: Looks good. There are conflicts now, please update again.
k8s-ci-robot: @sbs2001: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
hangyan: And we may need some docs update. You can go thourough the docs and find is there anything we can update.
sbs2001: @hangyan  PR is rebased
hangyan: Looks good now. I'm gonna merge this. If there are any further updates or other thoughts, we can create new a  PR.
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/kompose/pull/1452#pullrequestreview-797237626" title="Approved">hangyan</a>*, *<a href="https://github.com/kubernetes/kompose/pull/1452#" title="Author self-approved">sbs2001</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fkompose).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes/kompose/blob/master/OWNERS)~~ [hangyan]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/docs/user-guide.md b/docs/user-guide.md
index 61754b3a..b0d2e690 100755
--- a/docs/user-guide.md
+++ b/docs/user-guide.md
@@ -94,7 +94,7 @@ INFO OpenShift file "foo-buildconfig.yaml" created
 
 ## Alternative Conversions
 
-The default `kompose` transformation will generate Kubernetes [Deployments](http://kubernetes.io/docs/user-guide/deployments/) and [Services](http://kubernetes.io/docs/user-guide/services/), in yaml format. You have alternative option to generate json with `-j`. Also, you can alternatively generate [Replication Controllers](http://kubernetes.io/docs/user-guide/replication-controller/) objects, [Daemon Sets](http://kubernetes.io/docs/admin/daemons/), or [Helm](https://github.com/helm/helm) charts.
+The default `kompose` transformation will generate Kubernetes [Deployments](http://kubernetes.io/docs/user-guide/deployments/) and [Services](http://kubernetes.io/docs/user-guide/services/), in yaml format. You have alternative option to generate json with `-j`. Also, you can alternatively generate [Replication Controllers](http://kubernetes.io/docs/user-guide/replication-controller/) objects, [Daemon Sets](http://kubernetes.io/docs/admin/daemons/), [Statefulset](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) or [Helm](https://github.com/helm/helm) charts.
 
 ```sh
 $ kompose convert -j
@@ -125,6 +125,17 @@ INFO Kubernetes file "web-daemonset.yaml" created
 
 The `*-daemonset.yaml` files contain the Daemon Set objects
 
+```sh
+$ kompose convert --controller statefulset
+INFO Kubernetes file "db-service.yaml" created    
+INFO Kubernetes file "wordpress-service.yaml" created 
+INFO Kubernetes file "db-statefulset.yaml" created 
+INFO Kubernetes file "wordpress-statefulset.yaml" created 
+```
+
+The `*statefulset-.yaml` files contain the Statefulset objects.
+
+
 If you want to generate a Chart to be used with [Helm](https://github.com/kubernetes/helm) simply do:
 
 ```sh
diff --git a/pkg/transformer/kubernetes/k8sutils.go b/pkg/transformer/kubernetes/k8sutils.go
index 774150d9..82e2d44e 100644
--- a/pkg/transformer/kubernetes/k8sutils.go
+++ b/pkg/transformer/kubernetes/k8sutils.go
@@ -496,7 +496,7 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic
 		volumesMount = append(volumesMount, TmpVolumesMount...)
 	}
 
-	if pvc != nil {
+	if pvc != nil && opt.Controller != StatefulStateController {
 		// Looping on the slice pvc instead of `*objects = append(*objects, pvc...)`
 		// because the type of objects and pvc is different, but when doing append
 		// one element at a time it gets converted to runtime.Object for objects slice
@@ -530,7 +530,9 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic
 		template.Spec.Containers[0].VolumeMounts = append(template.Spec.Containers[0].VolumeMounts, volumesMount...)
 		template.Spec.Containers[0].Stdin = service.Stdin
 		template.Spec.Containers[0].TTY = service.Tty
-		template.Spec.Volumes = append(template.Spec.Volumes, volumes...)
+		if opt.Controller != StatefulStateController || opt.Volumes == "configMap" {
+			template.Spec.Volumes = append(template.Spec.Volumes, volumes...)
+		}
 		template.Spec.Affinity = ConfigAffinity(service)
 		// Configure the HealthCheck
 		template.Spec.Containers[0].LivenessProbe = configProbe(service.HealthChecks.Liveness)
@@ -637,6 +639,18 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic
 				objType.Spec.Strategy.Type = appsv1.RecreateDeploymentStrategyType
 			case *deployapi.DeploymentConfig:
 				objType.Spec.Strategy.Type = deployapi.DeploymentStrategyTypeRecreate
+			case *appsv1.StatefulSet:
+				// embed all PVCs inside the StatefulSet object
+				if opt.Volumes == "configMap" {
+					break
+				}
+				persistentVolumeClaims := make([]api.PersistentVolumeClaim, len(pvc))
+				for i, persistentVolumeClaim := range pvc {
+					persistentVolumeClaims[i] = *persistentVolumeClaim
+					persistentVolumeClaims[i].APIVersion = ""
+					persistentVolumeClaims[i].Kind = ""
+				}
+				objType.Spec.VolumeClaimTemplates = persistentVolumeClaims
 			}
 		}
 	}
diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go
index c271bebe..065319e7 100644
--- a/pkg/transformer/kubernetes/kubernetes.go
+++ b/pkg/transformer/kubernetes/kubernetes.go
@@ -63,6 +63,8 @@ const (
 	DeploymentController = "deployment"
 	// DaemonSetController is controller type for DaemonSet
 	DaemonSetController = "daemonset"
+	// StatefulStateController is controller type for StatefulSet
+	StatefulStateController = "statefulset"
 )
 
 // CheckUnsupportedKey checks if given komposeObject contains
@@ -425,6 +427,37 @@ func (k *Kubernetes) InitDS(name string, service kobject.ServiceConfig) *appsv1.
 	return ds
 }
 
+func (k *Kubernetes) InitSS(name string, service kobject.ServiceConfig, replicas int) *appsv1.StatefulSet {
+	var podSpec api.PodSpec
+	if len(service.Configs) > 0 {
+		podSpec = k.InitPodSpecWithConfigMap(name, service.Image, service)
+	} else {
+		podSpec = k.InitPodSpec(name, service.Image, service.ImagePullSecret)
+	}
+	rp := int32(replicas)
+	ds := &appsv1.StatefulSet{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "StatefulSet",
+			APIVersion: "apps/v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:   name,
+			Labels: transformer.ConfigAllLabels(name, &service),
+		},
+		Spec: appsv1.StatefulSetSpec{
+			Replicas: &rp,
+			Template: api.PodTemplateSpec{
+				Spec: podSpec,
+			},
+			Selector: &metav1.LabelSelector{
+				MatchLabels: transformer.ConfigLabels(name),
+			},
+			ServiceName: service.Name,
+		},
+	}
+	return ds
+}
+
 func (k *Kubernetes) initIngress(name string, service kobject.ServiceConfig, port int32) *networkingv1.Ingress {
 	hosts := regexp.MustCompile("[ ,]*,[ ,]*").Split(service.ExposeService, -1)
 
@@ -1120,6 +1153,10 @@ func (k *Kubernetes) CreateWorkloadAndConfigMapObjects(name string, service kobj
 		objects = append(objects, k.InitDS(name, service))
 	}
 
+	if opt.Controller == StatefulStateController {
+		objects = append(objects, k.InitSS(name, service, replica))
+	}
+
 	if len(service.EnvFile) > 0 {
 		for _, envFile := range service.EnvFile {
 			configMap := k.InitConfigMapForEnv(name, opt, envFile)
@@ -1280,7 +1317,6 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject.
 			allobjects = append(allobjects, item)
 		}
 	}
-
 	if opt.ServiceGroupMode != "" {
 		log.Debugf("Service group mode is: %s", opt.ServiceGroupMode)
 		komposeObjectToServiceConfigGroupMapping := KomposeObjectToServiceConfigGroupMapping(&komposeObject, opt)
@@ -1413,6 +1449,9 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject.
 			objects = k.CreateWorkloadAndConfigMapObjects(name, service, opt)
 		}
 
+		if opt.Controller == StatefulStateController {
+			service.ServiceType = "Headless"
+		}
 		k.configKubeServiceAndIngressForService(service, name, &objects)
 
 		err := k.UpdateKubernetesObjects(name, service, opt, &objects)
@@ -1449,6 +1488,12 @@ func (k *Kubernetes) UpdateController(obj runtime.Object, updateTemplate func(*a
 			return errors.Wrap(err, "updateTemplate failed")
 		}
 		updateMeta(&t.ObjectMeta)
+	case *appsv1.StatefulSet:
+		err = updateTemplate(&t.Spec.Template)
+		if err != nil {
+			return errors.Wrap(err, "updateTemplate failed")
+		}
+		updateMeta(&t.ObjectMeta)
 	case *deployapi.DeploymentConfig:
 		err = updateTemplate(t.Spec.Template)
 		if err != nil {


Test patch:
diff --git a/pkg/transformer/kubernetes/kubernetes_test.go b/pkg/transformer/kubernetes/kubernetes_test.go
index ab04da0a..110b0c09 100644
--- a/pkg/transformer/kubernetes/kubernetes_test.go
+++ b/pkg/transformer/kubernetes/kubernetes_test.go
@@ -303,7 +303,9 @@ func TestKomposeConvert(t *testing.T) {
 		// objects generated are deployment, daemonset, ReplicationController, service and pvc
 		"Convert to D, DS, and RC":                  {newKomposeObject(), kobject.ConvertOptions{CreateD: true, CreateDS: true, CreateRC: true, Replicas: replicas, IsReplicaSetFlag: true}, 7},
 		"Convert to D, DS, and RC with v3 replicas": {newKomposeObject(), kobject.ConvertOptions{CreateD: true, CreateDS: true, CreateRC: true}, 7},
-		// TODO: add more tests
+		// objects generated are statefulset
+		"Convert to SS with replicas ":   {newKomposeObject(), kobject.ConvertOptions{Controller: StatefulStateController, Replicas: replicas, IsReplicaSetFlag: true}, 5},
+		"Convert to SS without replicas": {newKomposeObject(), kobject.ConvertOptions{Controller: StatefulStateController}, 5},
 	}
 
 	for name, test := range testCases {
@@ -318,7 +320,7 @@ func TestKomposeConvert(t *testing.T) {
 			t.Errorf("Expected %d objects returned, got %d", test.expectedNumObjs, len(objs))
 		}
 
-		var foundSVC, foundD, foundDS, foundDC bool
+		var foundSVC, foundD, foundDS, foundDC, foundSS bool
 		name := "app"
 		labels := transformer.ConfigLabels(name)
 		config := test.komposeObject.ServiceConfigs[name]
@@ -334,6 +336,7 @@ func TestKomposeConvert(t *testing.T) {
 				}
 				foundSVC = true
 			}
+
 			if test.opt.CreateD {
 				if d, ok := obj.(*appsv1.Deployment); ok {
 					if err := checkPodTemplate(config, d.Spec.Template, labelsWithNetwork); err != nil {
@@ -426,6 +429,60 @@ func TestKomposeConvert(t *testing.T) {
 				}
 			}
 
+			if test.opt.Controller == StatefulStateController {
+				if ss, ok := obj.(*appsv1.StatefulSet); ok {
+					if err := checkPodTemplate(config, ss.Spec.Template, labelsWithNetwork); err != nil {
+						t.Errorf("%v", err)
+					}
+					if err := checkMeta(config, ss.ObjectMeta, name, true); err != nil {
+						t.Errorf("%v", err)
+					}
+					if test.opt.IsReplicaSetFlag {
+						if (int)(*ss.Spec.Replicas) != replicas {
+							t.Errorf("Expected %d replicas, got %d", replicas, ss.Spec.Replicas)
+						}
+					} else {
+						if (int)(*ss.Spec.Replicas) != newServiceConfig().Replicas {
+							t.Errorf("Expected %d replicas, got %d", newServiceConfig().Replicas, ss.Spec.Replicas)
+						}
+					}
+					foundSS = true
+				}
+
+				if u, ok := obj.(*unstructured.Unstructured); ok {
+					if u.GetKind() == "Statefulset" {
+						u.SetGroupVersionKind(schema.GroupVersionKind{
+							Group:   "apps",
+							Version: "v1",
+							Kind:    "Statefulset",
+						})
+						data, err := json.Marshal(u)
+						if err != nil {
+							t.Errorf("%v", err)
+						}
+						var d appsv1.Deployment
+						if err := json.Unmarshal(data, &d); err == nil {
+							if err := checkPodTemplate(config, d.Spec.Template, labelsWithNetwork); err != nil {
+								t.Errorf("%v", err)
+							}
+							if err := checkMeta(config, d.ObjectMeta, name, true); err != nil {
+								t.Errorf("%v", err)
+							}
+							if test.opt.IsReplicaSetFlag {
+								if (int)(*d.Spec.Replicas) != replicas {
+									t.Errorf("Expected %d replicas, got %d", replicas, d.Spec.Replicas)
+								}
+							} else {
+								if (int)(*d.Spec.Replicas) != newServiceConfig().Replicas {
+									t.Errorf("Expected %d replicas, got %d", newServiceConfig().Replicas, d.Spec.Replicas)
+								}
+							}
+							foundSS = true
+						}
+					}
+				}
+			}
+
 			// TODO: k8s & openshift transformer is now separated; either separate the test or combine the transformer
 			if test.opt.CreateDeploymentConfig {
 				if dc, ok := obj.(*deployapi.DeploymentConfig); ok {
@@ -455,6 +512,9 @@ func TestKomposeConvert(t *testing.T) {
 			t.Errorf("Expected create Daemon Set: %v, found Daemon Set: %v", test.opt.CreateDS, foundDS)
 		}
 
+		if test.opt.Controller == StatefulStateController && !foundSS {
+			t.Errorf("Expected create StatefulStateController")
+		}
 		if test.opt.CreateDeploymentConfig != foundDC {
 			t.Errorf("Expected create Deployment Config: %v, found Deployment Config: %v", test.opt.CreateDeploymentConfig, foundDC)
 		}
diff --git a/script/test/cmd/tests_new.sh b/script/test/cmd/tests_new.sh
index 16fa3863..980b9cf0 100755
--- a/script/test/cmd/tests_new.sh
+++ b/script/test/cmd/tests_new.sh
@@ -178,4 +178,12 @@ os_cmd="kompose --provider=openshift -f $KOMPOSE_ROOT/script/test/fixtures/healt
 k8s_output="$KOMPOSE_ROOT/script/test/fixtures/healthcheck/output-healthcheck-k8s.json"
 os_output="$KOMPOSE_ROOT/script/test/fixtures/healthcheck/output-healthcheck-os.json"
 convert::expect_success "$k8s_cmd" "$k8s_output"
-convert::expect_success "$os_cmd" "$os_output"
\ No newline at end of file
+convert::expect_success "$os_cmd" "$os_output"
+
+# test statefulset
+k8s_cmd="kompose -f $KOMPOSE_ROOT/script/test/fixtures/statefulset/docker-compose.yaml convert --stdout -j --with-kompose-annotation=false --controller statefulset"
+ocp_cmd="kompose --provider=openshift -f $KOMPOSE_ROOT/script/test/fixtures/statefulset/docker-compose.yaml convert --stdout -j --with-kompose-annotation=false --controller statefulset"
+k8s_output="$KOMPOSE_ROOT/script/test/fixtures/statefulset/output-k8s.json"
+ocp_output="$KOMPOSE_ROOT/script/test/fixtures/statefulset/output-os.json"
+convert::expect_success "$k8s_cmd" "$k8s_output"
+convert::expect_success "$ocp_cmd" "$ocp_output"
diff --git a/script/test/fixtures/statefulset/docker-compose.yaml b/script/test/fixtures/statefulset/docker-compose.yaml
new file mode 100644
index 00000000..489a2c43
--- /dev/null
+++ b/script/test/fixtures/statefulset/docker-compose.yaml
@@ -0,0 +1,33 @@
+version: "3"
+    
+services:
+  db:
+    image: mysql:5.7
+    volumes:
+      - db_data:/var/lib/mysql
+    restart: always
+    environment:
+      MYSQL_ROOT_PASSWORD: somewordpress
+      MYSQL_DATABASE: wordpress
+      MYSQL_USER: wordpress
+      MYSQL_PASSWORD: wordpress
+    ports:
+      - "3306:3306"
+    
+  wordpress:
+    depends_on:
+      - db
+    image: wordpress:latest
+    volumes:
+      - wordpress_data:/var/www/html
+    ports:
+      - "8000:80"
+    restart: always
+    environment:
+      WORDPRESS_DB_HOST: db:3306
+      WORDPRESS_DB_USER: wordpress
+      WORDPRESS_DB_PASSWORD: wordpress
+      WORDPRESS_DB_NAME: wordpress
+volumes:
+  db_data: {}
+  wordpress_data: {}
\ No newline at end of file
diff --git a/script/test/fixtures/statefulset/output-k8s.json b/script/test/fixtures/statefulset/output-k8s.json
new file mode 100644
index 00000000..5c1e339a
--- /dev/null
+++ b/script/test/fixtures/statefulset/output-k8s.json
@@ -0,0 +1,249 @@
+{
+  "kind": "List",
+  "apiVersion": "v1",
+  "metadata": {},
+  "items": [
+    {
+      "kind": "Service",
+      "apiVersion": "v1",
+      "metadata": {
+        "name": "db",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "db"
+        }
+      },
+      "spec": {
+        "ports": [
+          {
+            "name": "3306",
+            "port": 3306,
+            "targetPort": 3306
+          }
+        ],
+        "selector": {
+          "io.kompose.service": "db"
+        },
+        "clusterIP": "None",
+        "type": "ClusterIP"
+      },
+      "status": {
+        "loadBalancer": {}
+      }
+    },
+    {
+      "kind": "Service",
+      "apiVersion": "v1",
+      "metadata": {
+        "name": "wordpress",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "wordpress"
+        }
+      },
+      "spec": {
+        "ports": [
+          {
+            "name": "8000",
+            "port": 8000,
+            "targetPort": 80
+          }
+        ],
+        "selector": {
+          "io.kompose.service": "wordpress"
+        },
+        "clusterIP": "None",
+        "type": "ClusterIP"
+      },
+      "status": {
+        "loadBalancer": {}
+      }
+    },
+    {
+      "kind": "StatefulSet",
+      "apiVersion": "apps/v1",
+      "metadata": {
+        "name": "db",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "db"
+        }
+      },
+      "spec": {
+        "replicas": 1,
+        "selector": {
+          "matchLabels": {
+            "io.kompose.service": "db"
+          }
+        },
+        "template": {
+          "metadata": {
+            "creationTimestamp": null,
+            "labels": {
+              "io.kompose.service": "db"
+            }
+          },
+          "spec": {
+            "containers": [
+              {
+                "name": "db",
+                "image": "mysql:5.7",
+                "ports": [
+                  {
+                    "containerPort": 3306
+                  }
+                ],
+                "env": [
+                  {
+                    "name": "MYSQL_DATABASE",
+                    "value": "wordpress"
+                  },
+                  {
+                    "name": "MYSQL_PASSWORD",
+                    "value": "wordpress"
+                  },
+                  {
+                    "name": "MYSQL_ROOT_PASSWORD",
+                    "value": "somewordpress"
+                  },
+                  {
+                    "name": "MYSQL_USER",
+                    "value": "wordpress"
+                  }
+                ],
+                "resources": {},
+                "volumeMounts": [
+                  {
+                    "name": "db-data",
+                    "mountPath": "/var/lib/mysql"
+                  }
+                ]
+              }
+            ],
+            "restartPolicy": "Always"
+          }
+        },
+        "volumeClaimTemplates": [
+          {
+            "metadata": {
+              "name": "db-data",
+              "creationTimestamp": null,
+              "labels": {
+                "io.kompose.service": "db-data"
+              }
+            },
+            "spec": {
+              "accessModes": [
+                "ReadWriteOnce"
+              ],
+              "resources": {
+                "requests": {
+                  "storage": "100Mi"
+                }
+              }
+            },
+            "status": {}
+          }
+        ],
+        "serviceName": "db",
+        "updateStrategy": {}
+      },
+      "status": {
+        "replicas": 0
+      }
+    },
+    {
+      "kind": "StatefulSet",
+      "apiVersion": "apps/v1",
+      "metadata": {
+        "name": "wordpress",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "wordpress"
+        }
+      },
+      "spec": {
+        "replicas": 1,
+        "selector": {
+          "matchLabels": {
+            "io.kompose.service": "wordpress"
+          }
+        },
+        "template": {
+          "metadata": {
+            "creationTimestamp": null,
+            "labels": {
+              "io.kompose.service": "wordpress"
+            }
+          },
+          "spec": {
+            "containers": [
+              {
+                "name": "wordpress",
+                "image": "wordpress:latest",
+                "ports": [
+                  {
+                    "containerPort": 80
+                  }
+                ],
+                "env": [
+                  {
+                    "name": "WORDPRESS_DB_HOST",
+                    "value": "db:3306"
+                  },
+                  {
+                    "name": "WORDPRESS_DB_NAME",
+                    "value": "wordpress"
+                  },
+                  {
+                    "name": "WORDPRESS_DB_PASSWORD",
+                    "value": "wordpress"
+                  },
+                  {
+                    "name": "WORDPRESS_DB_USER",
+                    "value": "wordpress"
+                  }
+                ],
+                "resources": {},
+                "volumeMounts": [
+                  {
+                    "name": "wordpress-data",
+                    "mountPath": "/var/www/html"
+                  }
+                ]
+              }
+            ],
+            "restartPolicy": "Always"
+          }
+        },
+        "volumeClaimTemplates": [
+          {
+            "metadata": {
+              "name": "wordpress-data",
+              "creationTimestamp": null,
+              "labels": {
+                "io.kompose.service": "wordpress-data"
+              }
+            },
+            "spec": {
+              "accessModes": [
+                "ReadWriteOnce"
+              ],
+              "resources": {
+                "requests": {
+                  "storage": "100Mi"
+                }
+              }
+            },
+            "status": {}
+          }
+        ],
+        "serviceName": "wordpress",
+        "updateStrategy": {}
+      },
+      "status": {
+        "replicas": 0
+      }
+    }
+  ]
+}
diff --git a/script/test/fixtures/statefulset/output-os.json b/script/test/fixtures/statefulset/output-os.json
new file mode 100644
index 00000000..0c40d367
--- /dev/null
+++ b/script/test/fixtures/statefulset/output-os.json
@@ -0,0 +1,503 @@
+{
+  "kind": "List",
+  "apiVersion": "v1",
+  "metadata": {},
+  "items": [
+    {
+      "kind": "Service",
+      "apiVersion": "v1",
+      "metadata": {
+        "name": "db",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "db"
+        }
+      },
+      "spec": {
+        "ports": [
+          {
+            "name": "3306",
+            "port": 3306,
+            "targetPort": 3306
+          }
+        ],
+        "selector": {
+          "io.kompose.service": "db"
+        }
+      },
+      "status": {
+        "loadBalancer": {}
+      }
+    },
+    {
+      "kind": "Service",
+      "apiVersion": "v1",
+      "metadata": {
+        "name": "wordpress",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "wordpress"
+        }
+      },
+      "spec": {
+        "ports": [
+          {
+            "name": "8000",
+            "port": 8000,
+            "targetPort": 80
+          }
+        ],
+        "selector": {
+          "io.kompose.service": "wordpress"
+        }
+      },
+      "status": {
+        "loadBalancer": {}
+      }
+    },
+    {
+      "kind": "StatefulSet",
+      "apiVersion": "apps/v1",
+      "metadata": {
+        "name": "db",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "db"
+        }
+      },
+      "spec": {
+        "replicas": 1,
+        "selector": {
+          "matchLabels": {
+            "io.kompose.service": "db"
+          }
+        },
+        "template": {
+          "metadata": {
+            "creationTimestamp": null,
+            "labels": {
+              "io.kompose.service": "db"
+            }
+          },
+          "spec": {
+            "containers": [
+              {
+                "name": "db",
+                "image": "mysql:5.7",
+                "ports": [
+                  {
+                    "containerPort": 3306
+                  }
+                ],
+                "env": [
+                  {
+                    "name": "MYSQL_DATABASE",
+                    "value": "wordpress"
+                  },
+                  {
+                    "name": "MYSQL_PASSWORD",
+                    "value": "wordpress"
+                  },
+                  {
+                    "name": "MYSQL_ROOT_PASSWORD",
+                    "value": "somewordpress"
+                  },
+                  {
+                    "name": "MYSQL_USER",
+                    "value": "wordpress"
+                  }
+                ],
+                "resources": {},
+                "volumeMounts": [
+                  {
+                    "name": "db-data",
+                    "mountPath": "/var/lib/mysql"
+                  }
+                ]
+              }
+            ],
+            "restartPolicy": "Always"
+          }
+        },
+        "volumeClaimTemplates": [
+          {
+            "metadata": {
+              "name": "db-data",
+              "creationTimestamp": null,
+              "labels": {
+                "io.kompose.service": "db-data"
+              }
+            },
+            "spec": {
+              "accessModes": [
+                "ReadWriteOnce"
+              ],
+              "resources": {
+                "requests": {
+                  "storage": "100Mi"
+                }
+              }
+            },
+            "status": {}
+          }
+        ],
+        "serviceName": "db",
+        "updateStrategy": {}
+      },
+      "status": {
+        "replicas": 0
+      }
+    },
+    {
+      "kind": "DeploymentConfig",
+      "apiVersion": "v1",
+      "metadata": {
+        "name": "db",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "db"
+        }
+      },
+      "spec": {
+        "strategy": {
+          "type": "Recreate",
+          "resources": {}
+        },
+        "triggers": [
+          {
+            "type": "ConfigChange"
+          },
+          {
+            "type": "ImageChange",
+            "imageChangeParams": {
+              "automatic": true,
+              "containerNames": [
+                "db"
+              ],
+              "from": {
+                "kind": "ImageStreamTag",
+                "name": "db:5.7"
+              }
+            }
+          }
+        ],
+        "replicas": 1,
+        "test": false,
+        "selector": {
+          "io.kompose.service": "db"
+        },
+        "template": {
+          "metadata": {
+            "creationTimestamp": null,
+            "labels": {
+              "io.kompose.service": "db"
+            }
+          },
+          "spec": {
+            "containers": [
+              {
+                "name": "db",
+                "image": " ",
+                "ports": [
+                  {
+                    "containerPort": 3306
+                  }
+                ],
+                "env": [
+                  {
+                    "name": "MYSQL_DATABASE",
+                    "value": "wordpress"
+                  },
+                  {
+                    "name": "MYSQL_PASSWORD",
+                    "value": "wordpress"
+                  },
+                  {
+                    "name": "MYSQL_ROOT_PASSWORD",
+                    "value": "somewordpress"
+                  },
+                  {
+                    "name": "MYSQL_USER",
+                    "value": "wordpress"
+                  }
+                ],
+                "resources": {},
+                "volumeMounts": [
+                  {
+                    "name": "db-data",
+                    "mountPath": "/var/lib/mysql"
+                  }
+                ]
+              }
+            ],
+            "restartPolicy": "Always"
+          }
+        }
+      },
+      "status": {
+        "latestVersion": 0,
+        "observedGeneration": 0,
+        "replicas": 0,
+        "updatedReplicas": 0,
+        "availableReplicas": 0,
+        "unavailableReplicas": 0
+      }
+    },
+    {
+      "kind": "ImageStream",
+      "apiVersion": "v1",
+      "metadata": {
+        "name": "db",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "db"
+        }
+      },
+      "spec": {
+        "lookupPolicy": {
+          "local": false
+        },
+        "tags": [
+          {
+            "name": "",
+            "annotations": null,
+            "from": {
+              "kind": "DockerImage",
+              "name": "mysql:5.7"
+            },
+            "generation": null,
+            "importPolicy": {},
+            "referencePolicy": {
+              "type": ""
+            }
+          }
+        ]
+      },
+      "status": {
+        "dockerImageRepository": ""
+      }
+    },
+    {
+      "kind": "StatefulSet",
+      "apiVersion": "apps/v1",
+      "metadata": {
+        "name": "wordpress",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "wordpress"
+        }
+      },
+      "spec": {
+        "replicas": 1,
+        "selector": {
+          "matchLabels": {
+            "io.kompose.service": "wordpress"
+          }
+        },
+        "template": {
+          "metadata": {
+            "creationTimestamp": null,
+            "labels": {
+              "io.kompose.service": "wordpress"
+            }
+          },
+          "spec": {
+            "containers": [
+              {
+                "name": "wordpress",
+                "image": "wordpress:latest",
+                "ports": [
+                  {
+                    "containerPort": 80
+                  }
+                ],
+                "env": [
+                  {
+                    "name": "WORDPRESS_DB_HOST",
+                    "value": "db:3306"
+                  },
+                  {
+                    "name": "WORDPRESS_DB_NAME",
+                    "value": "wordpress"
+                  },
+                  {
+                    "name": "WORDPRESS_DB_PASSWORD",
+                    "value": "wordpress"
+                  },
+                  {
+                    "name": "WORDPRESS_DB_USER",
+                    "value": "wordpress"
+                  }
+                ],
+                "resources": {},
+                "volumeMounts": [
+                  {
+                    "name": "wordpress-data",
+                    "mountPath": "/var/www/html"
+                  }
+                ]
+              }
+            ],
+            "restartPolicy": "Always"
+          }
+        },
+        "volumeClaimTemplates": [
+          {
+            "metadata": {
+              "name": "wordpress-data",
+              "creationTimestamp": null,
+              "labels": {
+                "io.kompose.service": "wordpress-data"
+              }
+            },
+            "spec": {
+              "accessModes": [
+                "ReadWriteOnce"
+              ],
+              "resources": {
+                "requests": {
+                  "storage": "100Mi"
+                }
+              }
+            },
+            "status": {}
+          }
+        ],
+        "serviceName": "wordpress",
+        "updateStrategy": {}
+      },
+      "status": {
+        "replicas": 0
+      }
+    },
+    {
+      "kind": "DeploymentConfig",
+      "apiVersion": "v1",
+      "metadata": {
+        "name": "wordpress",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "wordpress"
+        }
+      },
+      "spec": {
+        "strategy": {
+          "type": "Recreate",
+          "resources": {}
+        },
+        "triggers": [
+          {
+            "type": "ConfigChange"
+          },
+          {
+            "type": "ImageChange",
+            "imageChangeParams": {
+              "automatic": true,
+              "containerNames": [
+                "wordpress"
+              ],
+              "from": {
+                "kind": "ImageStreamTag",
+                "name": "wordpress:latest"
+              }
+            }
+          }
+        ],
+        "replicas": 1,
+        "test": false,
+        "selector": {
+          "io.kompose.service": "wordpress"
+        },
+        "template": {
+          "metadata": {
+            "creationTimestamp": null,
+            "labels": {
+              "io.kompose.service": "wordpress"
+            }
+          },
+          "spec": {
+            "containers": [
+              {
+                "name": "wordpress",
+                "image": " ",
+                "ports": [
+                  {
+                    "containerPort": 80
+                  }
+                ],
+                "env": [
+                  {
+                    "name": "WORDPRESS_DB_HOST",
+                    "value": "db:3306"
+                  },
+                  {
+                    "name": "WORDPRESS_DB_NAME",
+                    "value": "wordpress"
+                  },
+                  {
+                    "name": "WORDPRESS_DB_PASSWORD",
+                    "value": "wordpress"
+                  },
+                  {
+                    "name": "WORDPRESS_DB_USER",
+                    "value": "wordpress"
+                  }
+                ],
+                "resources": {},
+                "volumeMounts": [
+                  {
+                    "name": "wordpress-data",
+                    "mountPath": "/var/www/html"
+                  }
+                ]
+              }
+            ],
+            "restartPolicy": "Always"
+          }
+        }
+      },
+      "status": {
+        "latestVersion": 0,
+        "observedGeneration": 0,
+        "replicas": 0,
+        "updatedReplicas": 0,
+        "availableReplicas": 0,
+        "unavailableReplicas": 0
+      }
+    },
+    {
+      "kind": "ImageStream",
+      "apiVersion": "v1",
+      "metadata": {
+        "name": "wordpress",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "wordpress"
+        }
+      },
+      "spec": {
+        "lookupPolicy": {
+          "local": false
+        },
+        "tags": [
+          {
+            "name": "",
+            "annotations": null,
+            "from": {
+              "kind": "DockerImage",
+              "name": "wordpress:latest"
+            },
+            "generation": null,
+            "importPolicy": {},
+            "referencePolicy": {
+              "type": ""
+            }
+          }
+        ]
+      },
+      "status": {
+        "dockerImageRepository": ""
+      }
+    }
+  ]
+}