From 9cc69e9e1026cb6c070c6f621292df3aaf571868 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Thu, 30 Apr 2026 17:56:10 -0400 Subject: [PATCH 1/2] feat: add dokku_proxy_property task Adds a property task that wraps `dokku proxy:set`, mirroring the existing property task family. Delegates to the shared `planProperty` helper so the task inherits drift probing, idempotency, and SSH error propagation. Closes #134. --- docs/dokku_proxy_property.md | 39 ++++++++ tasks/main_test.go | 2 +- tasks/proxy_property_task.go | 90 +++++++++++++++++++ tasks/proxy_property_task_integration_test.go | 62 +++++++++++++ tasks/proxy_property_task_test.go | 71 +++++++++++++++ 5 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 docs/dokku_proxy_property.md create mode 100644 tasks/proxy_property_task.go create mode 100644 tasks/proxy_property_task_integration_test.go create mode 100644 tasks/proxy_property_task_test.go diff --git a/docs/dokku_proxy_property.md b/docs/dokku_proxy_property.md new file mode 100644 index 0000000..32203cb --- /dev/null +++ b/docs/dokku_proxy_property.md @@ -0,0 +1,39 @@ +# dokku_proxy_property + +Manages the proxy configuration for a given dokku application + +## Setting the proxy type for an app + +```yaml +dokku_proxy_property: + app: node-js-app + property: type + value: nginx +``` + +## Setting the proxy type globally + +```yaml +dokku_proxy_property: + app: "" + global: true + property: type + value: haproxy +``` + +## Setting the proxy port for an app + +```yaml +dokku_proxy_property: + app: node-js-app + property: proxy-port + value: "8080" +``` + +## Clearing the proxy type for an app + +```yaml +dokku_proxy_property: + app: node-js-app + property: type +``` diff --git a/tasks/main_test.go b/tasks/main_test.go index cb5b360..a85f12e 100644 --- a/tasks/main_test.go +++ b/tasks/main_test.go @@ -487,7 +487,7 @@ func TestAllTasksExamplesReturnNoError(t *testing.T) { } func TestRegisteredTaskCount(t *testing.T) { - expected := 54 + expected := 55 if got := len(RegisteredTasks); got != expected { t.Errorf("expected %d registered tasks, got %d", expected, got) } diff --git a/tasks/proxy_property_task.go b/tasks/proxy_property_task.go new file mode 100644 index 0000000..8c92b20 --- /dev/null +++ b/tasks/proxy_property_task.go @@ -0,0 +1,90 @@ +package tasks + +// ProxyPropertyTask manages the proxy configuration for a given dokku application +type ProxyPropertyTask struct { + // App is the name of the app. Required if Global is false. + App string `required:"false" yaml:"app"` + + // Global is a flag indicating if the proxy configuration should be applied globally + Global bool `required:"false" yaml:"global,omitempty"` + + // Property is the name of the proxy property to set + Property string `required:"true" yaml:"property"` + + // Value is the value to set for the proxy property + Value string `required:"false" yaml:"value,omitempty"` + + // State is the desired state of the proxy configuration + State State `required:"true" yaml:"state,omitempty" default:"present" options:"present,absent"` +} + +// ProxyPropertyTaskExample contains an example of a ProxyPropertyTask +type ProxyPropertyTaskExample struct { + // Name is the task name holding the ProxyPropertyTask description + Name string `yaml:"-"` + + // ProxyPropertyTask is the ProxyPropertyTask configuration + ProxyPropertyTask ProxyPropertyTask `yaml:"dokku_proxy_property"` +} + +// GetName returns the name of the example +func (e ProxyPropertyTaskExample) GetName() string { + return e.Name +} + +// Doc returns the docblock for the proxy property task +func (t ProxyPropertyTask) Doc() string { + return "Manages the proxy configuration for a given dokku application" +} + +// Examples returns the examples for the proxy property task +func (t ProxyPropertyTask) Examples() ([]Doc, error) { + return MarshalExamples([]ProxyPropertyTaskExample{ + { + Name: "Setting the proxy type for an app", + ProxyPropertyTask: ProxyPropertyTask{ + App: "node-js-app", + Property: "type", + Value: "nginx", + }, + }, + { + Name: "Setting the proxy type globally", + ProxyPropertyTask: ProxyPropertyTask{ + Global: true, + Property: "type", + Value: "haproxy", + }, + }, + { + Name: "Setting the proxy port for an app", + ProxyPropertyTask: ProxyPropertyTask{ + App: "node-js-app", + Property: "proxy-port", + Value: "8080", + }, + }, + { + Name: "Clearing the proxy type for an app", + ProxyPropertyTask: ProxyPropertyTask{ + App: "node-js-app", + Property: "type", + }, + }, + }) +} + +// Execute sets or unsets the proxy property +func (t ProxyPropertyTask) Execute() TaskOutputState { + return ExecutePlan(t.Plan()) +} + +// Plan reports the drift the ProxyPropertyTask would produce. +func (t ProxyPropertyTask) Plan() PlanResult { + return planProperty(t.State, t.App, t.Global, t.Property, t.Value, "proxy:set") +} + +// init registers the ProxyPropertyTask with the task registry +func init() { + RegisterTask(&ProxyPropertyTask{}) +} diff --git a/tasks/proxy_property_task_integration_test.go b/tasks/proxy_property_task_integration_test.go new file mode 100644 index 0000000..0da040d --- /dev/null +++ b/tasks/proxy_property_task_integration_test.go @@ -0,0 +1,62 @@ +package tasks + +import ( + "testing" +) + +func TestIntegrationProxyProperty(t *testing.T) { + skipIfNoDokkuT(t) + + appName := "docket-test-proxy-prop" + + destroyApp(appName) + createApp(appName) + defer destroyApp(appName) + + // set proxy type + setTask := ProxyPropertyTask{ + App: appName, + Property: "type", + Value: "nginx", + State: StatePresent, + } + result := setTask.Execute() + if result.Error != nil { + t.Fatalf("failed to set proxy property: %v", result.Error) + } + if result.State != StatePresent { + t.Errorf("expected state 'present', got '%s'", result.State) + } + + // re-applying the same value should be a no-op + result = setTask.Execute() + if result.Error != nil { + t.Fatalf("failed to re-apply proxy property: %v", result.Error) + } + if result.Changed { + t.Errorf("expected re-apply to report Changed=false") + } + + // unset proxy type + unsetTask := ProxyPropertyTask{ + App: appName, + Property: "type", + State: StateAbsent, + } + result = unsetTask.Execute() + if result.Error != nil { + t.Fatalf("failed to unset proxy property: %v", result.Error) + } + if result.State != StateAbsent { + t.Errorf("expected state 'absent', got '%s'", result.State) + } + + // re-applying the absent state should be a no-op + result = unsetTask.Execute() + if result.Error != nil { + t.Fatalf("failed to re-apply absent proxy property: %v", result.Error) + } + if result.Changed { + t.Errorf("expected re-apply absent to report Changed=false") + } +} diff --git a/tasks/proxy_property_task_test.go b/tasks/proxy_property_task_test.go new file mode 100644 index 0000000..3549494 --- /dev/null +++ b/tasks/proxy_property_task_test.go @@ -0,0 +1,71 @@ +package tasks + +import ( + "strings" + "testing" +) + +func TestProxyPropertyTaskInvalidState(t *testing.T) { + task := ProxyPropertyTask{App: "test-app", Property: "type", State: "invalid"} + result := task.Execute() + if result.Error == nil { + t.Fatal("Execute with invalid state should return an error") + } +} + +func TestProxyPropertyTaskMissingApp(t *testing.T) { + task := ProxyPropertyTask{Property: "type", Value: "nginx", State: StatePresent} + result := task.Execute() + if result.Error == nil { + t.Fatal("Execute without app and global=false should return an error") + } +} + +func TestProxyPropertyTaskGlobalWithAppSet(t *testing.T) { + task := ProxyPropertyTask{ + App: "test-app", + Global: true, + Property: "type", + Value: "nginx", + State: StatePresent, + } + result := task.Execute() + if result.Error == nil { + t.Fatal("expected error when both global and app are set") + } + if !strings.Contains(result.Error.Error(), "must not be set when 'global' is set to true") { + t.Errorf("unexpected error: %v", result.Error) + } +} + +func TestProxyPropertyTaskPresentWithoutValue(t *testing.T) { + task := ProxyPropertyTask{ + App: "test-app", + Property: "type", + Value: "", + State: StatePresent, + } + result := task.Execute() + if result.Error == nil { + t.Fatal("expected error when present state has no value") + } + if !strings.Contains(result.Error.Error(), "invalid without a value") { + t.Errorf("unexpected error: %v", result.Error) + } +} + +func TestProxyPropertyTaskAbsentWithValue(t *testing.T) { + task := ProxyPropertyTask{ + App: "test-app", + Property: "type", + Value: "nginx", + State: StateAbsent, + } + result := task.Execute() + if result.Error == nil { + t.Fatal("expected error when absent state has a value") + } + if !strings.Contains(result.Error.Error(), "invalid with a value") { + t.Errorf("unexpected error: %v", result.Error) + } +} From 256c9913af337b457e2e7c9b53052d134e6983d9 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Thu, 30 Apr 2026 18:18:37 -0400 Subject: [PATCH 2/2] ci: raise integration test timeout to 20m The default 10m package timeout was tripped after the cumulative integration suite grew past it. Lifting the cap leaves ample headroom without masking real hangs. --- .github/workflows/test.yml | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd26ff5..e82573d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,7 +59,7 @@ jobs: - name: install dokku acl plugin run: sudo dokku plugin:install https://github.com/dokku-community/dokku-acl.git acl - name: run integration tests - run: sudo go test -v -count=1 -run TestIntegration ./tasks/ + run: sudo go test -v -count=1 -timeout 20m -run TestIntegration ./tasks/ bats-test: name: bats-test diff --git a/Makefile b/Makefile index 777048a..07949e5 100644 --- a/Makefile +++ b/Makefile @@ -215,4 +215,4 @@ test: .PHONY: test-integration test-integration: - go test -v -count=1 -run TestIntegration ./tasks/ + go test -v -count=1 -timeout 20m -run TestIntegration ./tasks/