Skip to content

Commit 4803cd7

Browse files
authored
Merge pull request #119 from dokku/15-implement-service-create-task
feat: implement service create task
2 parents b8d5787 + 992a761 commit 4803cd7

6 files changed

Lines changed: 403 additions & 1 deletion

File tree

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,7 @@ jobs:
3838
sudo apt-get update -qq
3939
sudo DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true apt-get install -qq -y dokku
4040
sudo dokku plugin:install-dependencies --core
41+
- name: install dokku redis plugin
42+
run: sudo dokku plugin:install https://github.com/dokku/dokku-redis.git redis
4143
- name: run integration tests
4244
run: sudo go test -v -count=1 -run TestIntegration ./tasks/

docs/dokku_service_create.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# dokku_service_create
2+
3+
Creates or destroys a dokku service
4+
5+
## Create a redis service named my-redis
6+
7+
```yaml
8+
dokku_service_create:
9+
service: redis
10+
name: my-redis
11+
```
12+
13+
## Create a postgres service named my-db
14+
15+
```yaml
16+
dokku_service_create:
17+
service: postgres
18+
name: my-db
19+
```
20+
21+
## Destroy a redis service named my-redis
22+
23+
```yaml
24+
dokku_service_create:
25+
service: redis
26+
name: my-redis
27+
state: absent
28+
```

tasks/integration_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package tasks
33
import (
44
"omakase/subprocess"
55
"os"
6+
"strings"
67
"testing"
78
)
89

@@ -25,6 +26,31 @@ func skipIfNoDokkuT(t *testing.T) {
2526
}
2627
}
2728

29+
func dokkuPluginInstalled(plugin string) bool {
30+
result, err := subprocess.CallExecCommand(subprocess.ExecCommandInput{
31+
Command: "dokku",
32+
Args: []string{"plugin:list"},
33+
})
34+
if err != nil {
35+
return false
36+
}
37+
38+
for _, line := range strings.Split(result.StdoutContents(), "\n") {
39+
fields := strings.Fields(line)
40+
if len(fields) > 0 && fields[0] == plugin {
41+
return true
42+
}
43+
}
44+
return false
45+
}
46+
47+
func skipIfPluginMissingT(t *testing.T, plugin string) {
48+
t.Helper()
49+
if !dokkuPluginInstalled(plugin) {
50+
t.Skipf("skipping integration test: dokku plugin %q not installed", plugin)
51+
}
52+
}
53+
2854
func TestIntegrationAppCreateAndDestroy(t *testing.T) {
2955
skipIfNoDokkuT(t)
3056

@@ -934,3 +960,64 @@ func TestIntegrationMultiTaskWorkflow(t *testing.T) {
934960
}
935961
}
936962
}
963+
964+
func TestIntegrationServiceCreateAndDestroy(t *testing.T) {
965+
skipIfNoDokkuT(t)
966+
skipIfPluginMissingT(t, "redis")
967+
968+
serviceName := "omakase-test-service"
969+
serviceType := "redis"
970+
971+
// ensure clean state
972+
destroyService(serviceType, serviceName)
973+
974+
// create the service
975+
task := ServiceCreateTask{Service: serviceType, Name: serviceName, State: StatePresent}
976+
result := task.Execute()
977+
if result.Error != nil {
978+
t.Fatalf("failed to create service: %v", result.Error)
979+
}
980+
if result.State != StatePresent {
981+
t.Errorf("expected state 'present', got '%s'", result.State)
982+
}
983+
if !result.Changed {
984+
t.Error("expected changed=true for new service creation")
985+
}
986+
987+
// creating again should be idempotent
988+
result = task.Execute()
989+
if result.Error != nil {
990+
t.Fatalf("idempotent create failed: %v", result.Error)
991+
}
992+
if result.Changed {
993+
t.Error("expected changed=false for existing service")
994+
}
995+
if result.State != StatePresent {
996+
t.Errorf("expected state 'present', got '%s'", result.State)
997+
}
998+
999+
// destroy the service
1000+
destroyTask := ServiceCreateTask{Service: serviceType, Name: serviceName, State: StateAbsent}
1001+
result = destroyTask.Execute()
1002+
if result.Error != nil {
1003+
t.Fatalf("failed to destroy service: %v", result.Error)
1004+
}
1005+
if result.State != StateAbsent {
1006+
t.Errorf("expected state 'absent', got '%s'", result.State)
1007+
}
1008+
if !result.Changed {
1009+
t.Error("expected changed=true for service destruction")
1010+
}
1011+
1012+
// destroying again should be idempotent
1013+
result = destroyTask.Execute()
1014+
if result.Error != nil {
1015+
t.Fatalf("idempotent destroy failed: %v", result.Error)
1016+
}
1017+
if result.Changed {
1018+
t.Error("expected changed=false for nonexistent service")
1019+
}
1020+
if result.State != StateAbsent {
1021+
t.Errorf("expected state 'absent', got '%s'", result.State)
1022+
}
1023+
}

tasks/main_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ func TestRegisteredTasksExist(t *testing.T) {
165165
"dokku_proxy_toggle",
166166
"dokku_resource_limit",
167167
"dokku_resource_reserve",
168+
"dokku_service_create",
168169
"dokku_storage_ensure",
169170
"dokku_storage_mount",
170171
}
@@ -626,3 +627,83 @@ func TestGetTasksResourceReserveTaskParsedCorrectly(t *testing.T) {
626627
t.Error("ClearBefore = false, want true (YAML value should be preserved)")
627628
}
628629
}
630+
631+
func TestGetTasksServiceCreateTaskParsedCorrectly(t *testing.T) {
632+
data := []byte(`---
633+
- tasks:
634+
- name: create redis service
635+
dokku_service_create:
636+
service: redis
637+
name: my-redis
638+
`)
639+
context := map[string]interface{}{}
640+
641+
tasks, err := GetTasks(data, context)
642+
if err != nil {
643+
t.Fatalf("GetTasks failed: %v", err)
644+
}
645+
646+
task := tasks.Get("create redis service")
647+
if task == nil {
648+
t.Fatal("task 'create redis service' not found")
649+
}
650+
651+
scTask, ok := task.(*ServiceCreateTask)
652+
if !ok {
653+
st, ok2 := task.(ServiceCreateTask)
654+
if !ok2 {
655+
t.Fatalf("task is not a ServiceCreateTask (type is %T)", task)
656+
}
657+
scTask = &st
658+
}
659+
660+
if scTask.Service != "redis" {
661+
t.Errorf("Service = %q, want %q", scTask.Service, "redis")
662+
}
663+
if scTask.Name != "my-redis" {
664+
t.Errorf("Name = %q, want %q", scTask.Name, "my-redis")
665+
}
666+
if scTask.DesiredState() != StatePresent {
667+
t.Errorf("expected default state 'present', got %q", scTask.DesiredState())
668+
}
669+
}
670+
671+
func TestGetTasksServiceCreateWithTemplateContext(t *testing.T) {
672+
data := []byte(`---
673+
- tasks:
674+
- name: create {{ .service_type }} service
675+
dokku_service_create:
676+
service: {{ .service_type }}
677+
name: {{ .service_name }}
678+
`)
679+
context := map[string]interface{}{
680+
"service_type": "postgres",
681+
"service_name": "my-db",
682+
}
683+
684+
tasks, err := GetTasks(data, context)
685+
if err != nil {
686+
t.Fatalf("GetTasks failed: %v", err)
687+
}
688+
689+
task := tasks.Get("create postgres service")
690+
if task == nil {
691+
t.Fatal("task 'create postgres service' not found")
692+
}
693+
694+
scTask, ok := task.(*ServiceCreateTask)
695+
if !ok {
696+
st, ok2 := task.(ServiceCreateTask)
697+
if !ok2 {
698+
t.Fatalf("task is not a ServiceCreateTask (type is %T)", task)
699+
}
700+
scTask = &st
701+
}
702+
703+
if scTask.Service != "postgres" {
704+
t.Errorf("Service = %q, want %q", scTask.Service, "postgres")
705+
}
706+
if scTask.Name != "my-db" {
707+
t.Errorf("Name = %q, want %q", scTask.Name, "my-db")
708+
}
709+
}

0 commit comments

Comments
 (0)