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/ 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) + } +}