{"id":13645370,"url":"https://github.com/erikh/duct","last_synced_at":"2025-08-18T01:36:55.256Z","repository":{"id":54908516,"uuid":"309066549","full_name":"erikh/duct","owner":"erikh","description":"docker-compose alike functionality directly from golang, for integration testing.","archived":false,"fork":false,"pushed_at":"2025-04-27T07:42:56.000Z","size":148,"stargazers_count":64,"open_issues_count":0,"forks_count":4,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-06-17T16:51:20.804Z","etag":null,"topics":["docker","docker-compose","framework","golang","integration-testing"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/erikh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-11-01T09:59:25.000Z","updated_at":"2025-04-27T07:42:50.000Z","dependencies_parsed_at":"2024-01-14T09:32:18.141Z","dependency_job_id":"411db5c7-1891-4b76-8850-aac581451913","html_url":"https://github.com/erikh/duct","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/erikh/duct","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erikh%2Fduct","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erikh%2Fduct/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erikh%2Fduct/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erikh%2Fduct/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/erikh","download_url":"https://codeload.github.com/erikh/duct/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erikh%2Fduct/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270932581,"owners_count":24670241,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-17T02:00:09.016Z","response_time":129,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["docker","docker-compose","framework","golang","integration-testing"],"created_at":"2024-08-02T01:02:34.131Z","updated_at":"2025-08-18T01:36:55.232Z","avatar_url":"https://github.com/erikh.png","language":"Go","funding_links":[],"categories":["Go"],"sub_categories":[],"readme":"# duct: a Golang integration testing helper for Docker\n\nduct uses structs similar in a fashion to the way `docker-compose` uses YAML to\nlaunch your containers. This is so much faster and easier to control; why shell\nout to `docker-compose` at all?\n\nCheck out the [godoc](https://pkg.go.dev/github.com/erikh/duct).\n\n## Example\n\n### Running Containers\n\nHere's how you might launch [Gitea](https://gitea.io) in duct:\n\n```go\npackage main\n\nimport (\n  \"context\"\n  \"log\"\n  \"net\"\n  \"testing\"\n  \"time\"\n\n  \"github.com/erikh/duct\"\n  dc \"github.com/fsouza/go-dockerclient\"\n)\n\nfunc TestStartGitea(t *testing.T) {\n  c := duct.New(duct.Manifest{\n    {\n      Name: \"gitea-postgres\",\n      Env: []string{\n        \"POSTGRES_USER=gitea\",\n        \"POSTGRES_PASSWORD=gitea\",\n        \"POSTGRES_DB=gitea\",\n      },\n      Image:    \"postgres:latest\",\n      BootWait: 2 * time.Second,\n    },\n    {\n      Name: \"gitea\",\n      Env: []string{\n        \"USER_UID=1000\",\n        \"USER_GID=1000\",\n        \"DB_TYPE=postgres\",\n        \"DB_HOST=gitea-postgres:5432\",\n        \"DB_USER=gitea\",\n        \"DB_NAME=gitea\",\n        \"DB_PASSWD=gitea\",\n        \"DOMAIN=gitea\",\n        \"ROOT_URL=http://gitea:11498\",\n        \"DISABLE_SSH=true\",\n        \"OAUTH2_ENABLE=true\",\n        \"OAUTH2_JWT_SECRET=mysecret\",\n        \"INSTALL_LOCK=true\",\n      },\n      Image:    \"gitea/gitea:1.12\",\n      BootWait: 2 * time.Second,\n      AliveFunc: func(ctx context.Context, client *dc.Client, id string) error {\n        for {\n          conn, err := net.Dial(\"tcp\", \"localhost:11498\")\n          if err != nil {\n            log.Printf(\"Error while dialing container: %v\", err)\n            time.Sleep(100 * time.Millisecond)\n            continue\n          }\n          conn.Close()\n          return nil\n        }\n      },\n      PostCommands: [][]string{\n        {\n          \"gitea\", \"admin\", \"create-user\",\n          \"--username\", \"erikh\",\n          \"--password\", \"erikh\",\n          \"--email\", \"erikh@example.org\",\n        },\n      },\n      PortForwards: map[int]int{\n        11498: 3000,\n      },\n    },\n  }, duct.WithNewNetwork(\"gitea-integration-test\"))\n\n  // Ctrl+C and SIGTERM will tear this down, and pass it up to the test suite\n  c.HandleSignals(true)\n\n  t.Cleanup(func() {\n    if err := c.Teardown(context.Background()); err != nil {\n      t.Fatal(err)\n    }\n  })\n\n  if err := c.Launch(context.Background()); err != nil {\n    t.Fatal(err)\n  }\n\n  // do something with gitea\n}\n```\n\n### Builder support\n\nduct has very basic builder and context support. Make sure to use the\n`LocalImage` flag when using these images in your container manifests so they\ndon't get pulled. Builds are logged to stderr in a very similar fashion to\n`docker build`.\n\n```go\nb := Builder{\n  \"test-image\": {\n    Dockerfile: \"testdata/Dockerfile.test\",\n    Context:    \".\",\n  },\n  \"test-image2\": {\n    Dockerfile: \"testdata/Dockerfile.test\",\n  },\n}\n\nif err := b.Run(context.Background()); err != nil {\n  t.Fatal(err)\n}\n\nc := New(Manifest{\n  {\n    Name:       \"test-image\",\n    Image:      \"test-image\",\n    LocalImage: true,\n  },\n}, WithNewNetwork(\"duct-test-network\"))\n\nif err := c.Launch(context.Background()); err != nil {\n  t.Fatal(err)\n}\n\nif err := c.Teardown(context.Background()); err != nil {\n  t.Fatal(err)\n}\n```\n\n### Example Log Output\n\nduct has nice logging so you can figure out what the heck is going on. From the code above:\n\n```\n=== RUN   TestStartGitea\n2020/10/31 23:38:35 Pulling docker image: [postgres:latest]\n2020/10/31 23:38:37 Creating container: [gitea-postgres]\n2020/10/31 23:38:37 Pulling docker image: [gitea/gitea:1.12]\n2020/10/31 23:38:38 Creating container: [gitea]\n2020/10/31 23:38:39 Starting container: [gitea-postgres]\n2020/10/31 23:38:39 Sleeping for 2s (requested by \"gitea-postgres\" bootWait parameter)\n2020/10/31 23:38:41 Starting container: [gitea]\n2020/10/31 23:38:41 Sleeping for 2s (requested by \"gitea\" bootWait parameter)\n2020/10/31 23:38:43 Running aliveFunc for gitea\n2020/10/31 23:38:43 AliveFunc for gitea completed\n2020/10/31 23:38:43 Running post-command [gitea admin create-user --username erikh --password erikh --email erikh@example.org] in container: [gitea]\n2020/11/01 06:38:43 ...dules/setting/git.go:93:newGit() [I] Git Version: 2.24.3, Wire Protocol Version 2 Enabled\n2020/11/01 06:38:43 ...m.io/xorm/core/db.go:154:QueryContext() [I] [SQL] SELECT count(*) FROM \"user\" WHERE (type=0) [] - 5.089469ms\n2020/11/01 06:38:43 ...m.io/xorm/core/tx.go:36:BeginTx() [I] [SQL] BEGIN TRANSACTION [] - 146.59µs\n2020/11/01 06:38:43 ...m.io/xorm/core/tx.go:157:QueryContext() [I] [SQL] SELECT \"id\", \"lower_name\", \"name\", \"full_name\", \"email\", \"keep_email_private\", \"email_notifications_preference\", \"passwd\", \"passwd_hash_algo\", \"must_change_password\", \"login_type\", \"login_source\", \"login_name\", \"type\", \"location\", \"website\", \"rands\", \"salt\", \"language\", \"description\", \"created_unix\", \"updated_unix\", \"last_login_unix\", \"last_repo_visibility\", \"max_repo_creation\", \"is_active\", \"is_admin\", \"is_restricted\", \"allow_git_hook\", \"allow_import_local\", \"allow_create_organization\", \"prohibit_login\", \"avatar\", \"avatar_email\", \"use_custom_avatar\", \"num_followers\", \"num_following\", \"num_stars\", \"num_repos\", \"num_teams\", \"num_members\", \"visibility\", \"repo_admin_change_team_access\", \"diff_view_style\", \"theme\" FROM \"user\" WHERE (id!=$1) AND \"lower_name\"=$2 LIMIT 1 [0 erikh] - 1.26592ms\n2020/11/01 06:38:43 ...m.io/xorm/core/tx.go:157:QueryContext() [I] [SQL] SELECT \"id\", \"lower_name\", \"name\", \"full_name\", \"email\", \"keep_email_private\", \"email_notifications_preference\", \"passwd\", \"passwd_hash_algo\", \"must_change_password\", \"login_type\", \"login_source\", \"login_name\", \"type\", \"location\", \"website\", \"rands\", \"salt\", \"language\", \"description\", \"created_unix\", \"updated_unix\", \"last_login_unix\", \"last_repo_visibility\", \"max_repo_creation\", \"is_active\", \"is_admin\", \"is_restricted\", \"allow_git_hook\", \"allow_import_local\", \"allow_create_organization\", \"prohibit_login\", \"avatar\", \"avatar_email\", \"use_custom_avatar\", \"num_followers\", \"num_following\", \"num_stars\", \"num_repos\", \"num_teams\", \"num_members\", \"visibility\", \"repo_admin_change_team_access\", \"diff_view_style\", \"theme\" FROM \"user\" WHERE (email=$1) LIMIT 1 [erikh@example.org] - 668.11µs\n2020/11/01 06:38:43 ...m.io/xorm/core/tx.go:157:QueryContext() [I] [SQL] SELECT \"id\", \"uid\", \"email\", \"is_activated\" FROM \"email_address\" WHERE (email=$1) LIMIT 1 [erikh@example.org] - 658.433µs\n2020/11/01 06:38:43 ...m.io/xorm/core/tx.go:157:QueryContext() [I] [SQL] INSERT INTO \"user\" (\"lower_name\",\"name\",\"full_name\",\"email\",\"keep_email_private\",\"email_notifications_preference\",\"passwd\",\"passwd_hash_algo\",\"must_change_password\",\"login_type\",\"login_source\",\"login_name\",\"type\",\"location\",\"website\",\"rands\",\"salt\",\"language\",\"description\",\"created_unix\",\"updated_unix\",\"last_login_unix\",\"last_repo_visibility\",\"max_repo_creation\",\"is_active\",\"is_admin\",\"is_restricted\",\"allow_git_hook\",\"allow_import_local\",\"allow_create_organization\",\"prohibit_login\",\"avatar\",\"avatar_email\",\"use_custom_avatar\",\"num_followers\",\"num_following\",\"num_stars\",\"num_repos\",\"num_teams\",\"num_members\",\"visibility\",\"repo_admin_change_team_access\",\"diff_view_style\",\"theme\") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44) RETURNING \"id\" [erikh erikh  erikh@example.org false enabled c9448c0f954aaa240ba79f1d77c38a167f83479af7a8509a0f52affa28a937dbd2edbe6b3d63ac551567e76659f2a42b410c pbkdf2 false 0 0  0   CNBdZZp36u BUK9GUe8Ih   1604212723 1604212723 0 false -1 true false false false false false false 7c26bac970c7b33ad8f3e5a905d82a0c erikh@example.org false 0 0 0 0 0 0 public false  gitea] - 1.112965ms\nNew user 'erikh' has been successfully created!\n2020/10/31 23:38:43 Killing container: [gitea-postgres]\n2020/10/31 23:38:44 Removing container: [gitea-postgres]\n2020/10/31 23:38:44 Killing container: [gitea]\n2020/10/31 23:38:44 Removing container: [gitea]\n--- PASS: TestStartGitea (9.36s)\nPASS\nok      github.com/erikh/tmp    9.364s\n```\n\n## Roadmap:\n\n- [ ] Better `*testing.T` integrations with e.g., Cleanup directly\n- [ ] Stdio handling and sniffing\n- [ ] Attach handling\n\n# Author\n\nErik Hollensbe \u003cgithub@hollensbe.org\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ferikh%2Fduct","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ferikh%2Fduct","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ferikh%2Fduct/lists"}