Skip to content

Commit

Permalink
Add support for statefulset controller. (#1452)
Browse files Browse the repository at this point in the history
Signed-off-by: Shivam Sandbhor <shivam.sandbhor@gmail.com>
  • Loading branch information
sbs2001 authored Nov 4, 2021
1 parent d55071e commit c921643
Show file tree
Hide file tree
Showing 8 changed files with 930 additions and 7 deletions.
13 changes: 12 additions & 1 deletion docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions pkg/transformer/kubernetes/k8sutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
}
}
Expand Down
47 changes: 46 additions & 1 deletion pkg/transformer/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
64 changes: 62 additions & 2 deletions pkg/transformer/kubernetes/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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]
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
10 changes: 9 additions & 1 deletion script/test/cmd/tests_new.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
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"
33 changes: 33 additions & 0 deletions script/test/fixtures/statefulset/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -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: {}
Loading

0 comments on commit c921643

Please sign in to comment.