Skip to content

Commit 5bbffc0

Browse files
authored
Merge pull request #122 from dokku/9-implement-network-task
feat: implement dokku_network task
2 parents 13d806d + 731fd2b commit 5bbffc0

5 files changed

Lines changed: 346 additions & 1 deletion

File tree

docs/dokku_network.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# dokku_network
2+
3+
Creates or destroys a Docker network
4+
5+
## Create a network named example-network
6+
7+
```yaml
8+
dokku_network:
9+
name: example-network
10+
```
11+
12+
## Destroy a network named example-network
13+
14+
```yaml
15+
dokku_network:
16+
name: example-network
17+
state: absent
18+
```

tasks/integration_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,104 @@ func TestIntegrationAppCreateAndDestroy(t *testing.T) {
204204
}
205205
}
206206

207+
func dockerNetworkExists(name string) bool {
208+
result, err := subprocess.CallExecCommand(subprocess.ExecCommandInput{
209+
Command: "docker",
210+
Args: []string{"network", "inspect", name, "--format", "{{.Name}}"},
211+
})
212+
if err != nil {
213+
return false
214+
}
215+
return strings.TrimSpace(result.StdoutContents()) == name
216+
}
217+
218+
func TestIntegrationNetworkCreateAndDestroy(t *testing.T) {
219+
skipIfNoDokkuT(t)
220+
221+
networkName := "omakase-test-network"
222+
223+
// ensure clean state
224+
destroyNetwork(networkName)
225+
226+
// verify network does not exist via docker cli
227+
if dockerNetworkExists(networkName) {
228+
t.Fatal("expected network to not exist before creation")
229+
}
230+
231+
// create the network
232+
task := NetworkTask{Name: networkName, State: StatePresent}
233+
result := task.Execute()
234+
if result.Error != nil {
235+
t.Fatalf("failed to create network: %v", result.Error)
236+
}
237+
if result.State != StatePresent {
238+
t.Errorf("expected state 'present', got '%s'", result.State)
239+
}
240+
if !result.Changed {
241+
t.Error("expected changed=true for new network creation")
242+
}
243+
244+
// verify network exists via docker cli
245+
if !dockerNetworkExists(networkName) {
246+
t.Fatal("expected network to exist after creation")
247+
}
248+
249+
// verify network driver via docker cli
250+
inspectResult, err := subprocess.CallExecCommand(subprocess.ExecCommandInput{
251+
Command: "docker",
252+
Args: []string{"network", "inspect", networkName, "--format", "{{.Driver}}"},
253+
})
254+
if err != nil {
255+
t.Fatalf("failed to inspect network driver: %v", err)
256+
}
257+
driver := strings.TrimSpace(inspectResult.StdoutContents())
258+
if driver != "bridge" {
259+
t.Errorf("expected network driver 'bridge', got '%s'", driver)
260+
}
261+
262+
// creating again should be idempotent
263+
result = task.Execute()
264+
if result.Error != nil {
265+
t.Fatalf("idempotent create failed: %v", result.Error)
266+
}
267+
if result.Changed {
268+
t.Error("expected changed=false for existing network")
269+
}
270+
if result.State != StatePresent {
271+
t.Errorf("expected state 'present', got '%s'", result.State)
272+
}
273+
274+
// destroy the network
275+
destroyTask := NetworkTask{Name: networkName, State: StateAbsent}
276+
result = destroyTask.Execute()
277+
if result.Error != nil {
278+
t.Fatalf("failed to destroy network: %v", result.Error)
279+
}
280+
if result.State != StateAbsent {
281+
t.Errorf("expected state 'absent', got '%s'", result.State)
282+
}
283+
if !result.Changed {
284+
t.Error("expected changed=true for network destruction")
285+
}
286+
287+
// verify network does not exist via docker cli after destroy
288+
if dockerNetworkExists(networkName) {
289+
t.Fatal("expected network to not exist after destruction")
290+
}
291+
292+
// destroying again should be idempotent
293+
result = destroyTask.Execute()
294+
if result.Error != nil {
295+
t.Fatalf("idempotent destroy failed: %v", result.Error)
296+
}
297+
if result.Changed {
298+
t.Error("expected changed=false for nonexistent network")
299+
}
300+
if result.State != StateAbsent {
301+
t.Errorf("expected state 'absent', got '%s'", result.State)
302+
}
303+
}
304+
207305
func TestIntegrationConfigSetAndUnset(t *testing.T) {
208306
skipIfNoDokkuT(t)
209307

tasks/main_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ func TestRegisteredTasksExist(t *testing.T) {
160160
"dokku_domains_toggle",
161161
"dokku_git_from_image",
162162
"dokku_git_sync",
163+
"dokku_network",
163164
"dokku_network_property",
164165
"dokku_ports",
165166
"dokku_proxy_toggle",
@@ -847,3 +848,39 @@ func TestGetTasksServiceLinkWithTemplateContext(t *testing.T) {
847848
t.Errorf("Name = %q, want %q", slTask.Name, "my-db")
848849
}
849850
}
851+
852+
func TestGetTasksNetworkTaskParsedCorrectly(t *testing.T) {
853+
data := []byte(`---
854+
- tasks:
855+
- name: create test network
856+
dokku_network:
857+
name: test-network
858+
`)
859+
context := map[string]interface{}{}
860+
861+
tasks, err := GetTasks(data, context)
862+
if err != nil {
863+
t.Fatalf("GetTasks failed: %v", err)
864+
}
865+
866+
task := tasks.Get("create test network")
867+
if task == nil {
868+
t.Fatal("task 'create test network' not found")
869+
}
870+
871+
netTask, ok := task.(*NetworkTask)
872+
if !ok {
873+
nt, ok2 := task.(NetworkTask)
874+
if !ok2 {
875+
t.Fatalf("task is not a NetworkTask (type is %T)", task)
876+
}
877+
netTask = &nt
878+
}
879+
880+
if netTask.Name != "test-network" {
881+
t.Errorf("Name = %q, want %q", netTask.Name, "test-network")
882+
}
883+
if netTask.DesiredState() != StatePresent {
884+
t.Errorf("expected default state 'present', got %q", netTask.DesiredState())
885+
}
886+
}

tasks/network_task.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package tasks
2+
3+
import (
4+
"fmt"
5+
"omakase/subprocess"
6+
7+
yaml "gopkg.in/yaml.v3"
8+
)
9+
10+
// NetworkTask creates or destroys a Docker network
11+
type NetworkTask struct {
12+
// Name is the name of the network
13+
Name string `required:"true" yaml:"name"`
14+
15+
// State is the state of the network
16+
State State `required:"false" yaml:"state,omitempty" default:"present" options:"present,absent"`
17+
}
18+
19+
// NetworkTaskExample contains an example of a NetworkTask
20+
type NetworkTaskExample struct {
21+
// Name is the task name holding the NetworkTask description
22+
Name string `yaml:"-"`
23+
24+
// DokkuNetwork is the NetworkTask configuration
25+
DokkuNetwork NetworkTask `yaml:"dokku_network"`
26+
}
27+
28+
// DesiredState returns the desired state of the network
29+
func (t NetworkTask) DesiredState() State {
30+
return t.State
31+
}
32+
33+
// Doc returns the docblock for the network task
34+
func (t NetworkTask) Doc() string {
35+
return "Creates or destroys a Docker network"
36+
}
37+
38+
// Examples returns a list of NetworkTaskExamples as yaml
39+
func (t NetworkTask) Examples() ([]Doc, error) {
40+
examples := []NetworkTaskExample{
41+
{
42+
Name: "Create a network named example-network",
43+
DokkuNetwork: NetworkTask{
44+
Name: "example-network",
45+
},
46+
},
47+
{
48+
Name: "Destroy a network named example-network",
49+
DokkuNetwork: NetworkTask{
50+
Name: "example-network",
51+
State: "absent",
52+
},
53+
},
54+
}
55+
56+
var output []Doc
57+
for _, example := range examples {
58+
b, err := yaml.Marshal(example)
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
output = append(output, Doc{
64+
Name: example.Name,
65+
Codeblock: string(b),
66+
})
67+
}
68+
69+
return output, nil
70+
}
71+
72+
// Execute creates or destroys a Docker network
73+
func (t NetworkTask) Execute() TaskOutputState {
74+
funcMap := map[State]func(string) TaskOutputState{
75+
"present": createNetwork,
76+
"absent": destroyNetwork,
77+
}
78+
79+
fn, ok := funcMap[t.State]
80+
if !ok {
81+
return TaskOutputState{
82+
Error: fmt.Errorf("invalid state: %s", t.State),
83+
}
84+
}
85+
return fn(t.Name)
86+
}
87+
88+
// networkExists checks if a Docker network exists
89+
func networkExists(name string) bool {
90+
result, err := subprocess.CallExecCommand(subprocess.ExecCommandInput{
91+
Command: "dokku",
92+
Args: []string{
93+
"--quiet",
94+
"network:exists",
95+
name,
96+
},
97+
})
98+
if err != nil {
99+
return false
100+
}
101+
102+
return result.ExitCode == 0
103+
}
104+
105+
// createNetwork creates a Docker network
106+
func createNetwork(name string) TaskOutputState {
107+
state := TaskOutputState{
108+
Changed: false,
109+
State: "absent",
110+
}
111+
if networkExists(name) {
112+
state.State = "present"
113+
return state
114+
}
115+
116+
result, err := subprocess.CallExecCommand(subprocess.ExecCommandInput{
117+
Command: "dokku",
118+
Args: []string{
119+
"--quiet",
120+
"network:create",
121+
name,
122+
},
123+
})
124+
if err != nil {
125+
state.Error = err
126+
state.Message = result.StderrContents()
127+
return state
128+
}
129+
130+
state.Changed = true
131+
state.State = "present"
132+
return state
133+
}
134+
135+
// destroyNetwork destroys a Docker network
136+
func destroyNetwork(name string) TaskOutputState {
137+
state := TaskOutputState{
138+
Changed: false,
139+
State: "present",
140+
}
141+
if !networkExists(name) {
142+
state.State = "absent"
143+
return state
144+
}
145+
146+
result, err := subprocess.CallExecCommand(subprocess.ExecCommandInput{
147+
Command: "dokku",
148+
Args: []string{
149+
"--quiet",
150+
"--force",
151+
"network:destroy",
152+
name,
153+
},
154+
})
155+
if err != nil {
156+
state.Error = err
157+
state.Message = result.StderrContents()
158+
return state
159+
}
160+
161+
state.Changed = true
162+
state.State = "absent"
163+
return state
164+
}
165+
166+
// init registers the NetworkTask with the task registry
167+
func init() {
168+
RegisterTask(&NetworkTask{})
169+
}

tasks/task_execute_test.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,26 @@ func TestGitFromImageTaskInvalidState(t *testing.T) {
7777
}
7878
}
7979

80+
func TestNetworkTaskInvalidState(t *testing.T) {
81+
task := NetworkTask{Name: "test-network", State: "invalid"}
82+
result := task.Execute()
83+
if result.Error == nil {
84+
t.Fatal("Execute with invalid state should return an error")
85+
}
86+
}
87+
88+
func TestNetworkTaskDesiredState(t *testing.T) {
89+
task := NetworkTask{Name: "test-network", State: StatePresent}
90+
if task.DesiredState() != StatePresent {
91+
t.Errorf("expected state 'present', got '%s'", task.DesiredState())
92+
}
93+
94+
task = NetworkTask{Name: "test-network", State: StateAbsent}
95+
if task.DesiredState() != StateAbsent {
96+
t.Errorf("expected state 'absent', got '%s'", task.DesiredState())
97+
}
98+
}
99+
80100
func TestNetworkPropertyTaskInvalidState(t *testing.T) {
81101
task := NetworkPropertyTask{App: "test-app", Property: "attach-post-create", State: "invalid"}
82102
result := task.Execute()
@@ -359,6 +379,8 @@ func TestAllTasksDesiredState(t *testing.T) {
359379
{"DomainsToggleTask absent", &DomainsToggleTask{App: "test", State: StateAbsent}, StateAbsent},
360380
{"GitFromImageTask deployed", &GitFromImageTask{App: "test", Image: "nginx", State: StateDeployed}, StateDeployed},
361381
{"GitSyncTask present", &GitSyncTask{App: "test", Remote: "https://example.com/repo", State: StatePresent}, StatePresent},
382+
{"NetworkTask present", &NetworkTask{Name: "test", State: StatePresent}, StatePresent},
383+
{"NetworkTask absent", &NetworkTask{Name: "test", State: StateAbsent}, StateAbsent},
362384
{"NetworkPropertyTask present", &NetworkPropertyTask{App: "test", Property: "bind-all-interfaces", State: StatePresent}, StatePresent},
363385
{"NetworkPropertyTask absent", &NetworkPropertyTask{App: "test", Property: "bind-all-interfaces", State: StateAbsent}, StateAbsent},
364386
{"PortsTask present", &PortsTask{App: "test", State: StatePresent}, StatePresent},
@@ -563,7 +585,7 @@ func TestAllTasksExamplesReturnNoError(t *testing.T) {
563585
}
564586

565587
func TestRegisteredTaskCount(t *testing.T) {
566-
expected := 17
588+
expected := 18
567589
if got := len(RegisteredTasks); got != expected {
568590
t.Errorf("expected %d registered tasks, got %d", expected, got)
569591
}
@@ -581,6 +603,7 @@ func TestTaskDocStrings(t *testing.T) {
581603
{&DomainsToggleTask{}, "Enables or disables the domains plugin for a given dokku application"},
582604
{&GitFromImageTask{}, "Deploys a git repository from a docker image"},
583605
{&GitSyncTask{}, "Syncs a git repository to a dokku application"},
606+
{&NetworkTask{}, "Creates or destroys a Docker network"},
584607
{&NetworkPropertyTask{}, "Manages the network property for a given dokku application"},
585608
{&PortsTask{}, "Manages the ports for a given dokku application"},
586609
{&PsScaleTask{}, "Manages the process scale for a given dokku application"},

0 commit comments

Comments
 (0)