├── .devcontainer └── devcontainer.json ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build-samples-json.yml │ ├── go.yml │ └── install.yml ├── .gitignore ├── .golangci.yaml ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── defang.code-workspace ├── default.nix ├── flake.lock ├── flake.nix ├── pkgs ├── defang │ ├── cli.nix │ └── default.nix └── npm │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── mocha.ts │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── cli.ts │ └── clilib.ts │ ├── test │ └── clilib.spec.ts │ ├── tsconfig.json │ └── tsconfig.test.json └── src ├── .dockerignore ├── .gitignore ├── .goreleaser.yml ├── Dockerfile ├── Makefile ├── README.md ├── bin ├── codesign.sh ├── completions.sh ├── install └── notarize.sh ├── buf.gen.yaml ├── cmd ├── cli │ ├── command │ │ ├── cd.go │ │ ├── collectUnsetEnvVars_test.go │ │ ├── color.go │ │ ├── commands.go │ │ ├── commands_test.go │ │ ├── compose.go │ │ ├── compose_test.go │ │ ├── configrandom.go │ │ ├── configrandom_test.go │ │ ├── deploymentinfo.go │ │ ├── deploymentinfo_test.go │ │ ├── estimate.go │ │ ├── estimate_test.go │ │ ├── exitcode.go │ │ ├── hint.go │ │ ├── mcp.go │ │ ├── mode.go │ │ ├── version.go │ │ └── version_test.go │ ├── main.go │ ├── main_test.go │ ├── setuidgid_other.go │ ├── setuidgid_windows.go │ └── version.go ├── crun │ └── main.go └── gendocs │ └── main.go ├── go.mod ├── go.sum ├── pkg ├── auth │ ├── auth.go │ ├── client.go │ ├── client_test.go │ ├── interceptor.go │ └── pkce.go ├── cert │ └── check.go ├── cli │ ├── activeDeployments_test.go │ ├── agree_tos.go │ ├── bootstrap.go │ ├── cert.go │ ├── cert_test.go │ ├── client │ │ ├── byoc │ │ │ ├── aws │ │ │ │ ├── byoc.go │ │ │ │ ├── byoc_integration_test.go │ │ │ │ ├── byoc_test.go │ │ │ │ ├── domain.go │ │ │ │ ├── domain_integration_test.go │ │ │ │ ├── domain_test.go │ │ │ │ ├── stream.go │ │ │ │ ├── subscribe.go │ │ │ │ ├── testdata │ │ │ │ │ ├── build-failure-o4epidtq3j3b.json │ │ │ │ │ ├── build-failure.events │ │ │ │ │ ├── failure-then-success-c1v6g2m5qlvm.json │ │ │ │ │ ├── failure-then-success.events │ │ │ │ │ ├── healthcheck-failure-8ui3h0yf5xqg.json │ │ │ │ │ ├── healthcheck-failure.events │ │ │ │ │ ├── processexit-failure-se3n0qmzhzpm.json │ │ │ │ │ ├── processexit-failure.events │ │ │ │ │ ├── success-f249u7ap07ef.json │ │ │ │ │ └── success.events │ │ │ │ ├── validation.go │ │ │ │ └── validation_test.go │ │ │ ├── baseclient.go │ │ │ ├── baseclient_test.go │ │ │ ├── common.go │ │ │ ├── common_test.go │ │ │ ├── do │ │ │ │ ├── byoc.go │ │ │ │ ├── config.go │ │ │ │ └── stream.go │ │ │ ├── gcp │ │ │ │ ├── byoc.go │ │ │ │ ├── byoc_test.go │ │ │ │ ├── query.go │ │ │ │ ├── stream.go │ │ │ │ └── stream_test.go │ │ │ └── parse.go │ │ ├── client.go │ │ ├── errors.go │ │ ├── grpc.go │ │ ├── grpc_logger.go │ │ ├── mock.go │ │ ├── playground.go │ │ ├── playground_test.go │ │ ├── projectName.go │ │ ├── projectName_test.go │ │ ├── provider.go │ │ ├── provider_test.go │ │ ├── retrier.go │ │ ├── retrier_test.go │ │ ├── state.go │ │ ├── state_other.go │ │ ├── state_test.go │ │ ├── state_windows.go │ │ └── waitForCdTaskExit.go │ ├── common.go │ ├── compose │ │ ├── GPUcounter.go │ │ ├── compose_test.go │ │ ├── config_detector.go │ │ ├── config_detector_test.go │ │ ├── constants.go │ │ ├── context.go │ │ ├── context_test.go │ │ ├── convert.go │ │ ├── convert_test.go │ │ ├── fixup.go │ │ ├── fixup_test.go │ │ ├── load_content.go │ │ ├── load_content_test.go │ │ ├── loader.go │ │ ├── loader_test.go │ │ ├── normalize.go │ │ ├── normalize_test.go │ │ ├── serviceNameReplacer.go │ │ ├── serviceNameReplacer_test.go │ │ ├── stateful.go │ │ ├── stateful_test.go │ │ ├── validate.go │ │ ├── validate_test.go │ │ ├── validation.go │ │ └── validation_test.go │ ├── composeDown.go │ ├── composeDown_test.go │ ├── composeUp.go │ ├── composeUp_test.go │ ├── configDelete.go │ ├── configDelete_test.go │ ├── configList.go │ ├── configList_test.go │ ├── configSet.go │ ├── configSet_test.go │ ├── connect.go │ ├── connect_test.go │ ├── debug.go │ ├── debug_test.go │ ├── delete.go │ ├── deploymentsList.go │ ├── deploymentsList_test.go │ ├── destroy_test.go │ ├── generate.go │ ├── getServices.go │ ├── getServices_test.go │ ├── getVersion.go │ ├── login.go │ ├── login_test.go │ ├── logout.go │ ├── new.go │ ├── new_test.go │ ├── preview.go │ ├── preview_test.go │ ├── sendMsg.go │ ├── subscribe.go │ ├── subscribe_test.go │ ├── tail.go │ ├── tailAndMonitor.go │ ├── tail_test.go │ ├── teardown.go │ ├── token.go │ ├── upgrade.go │ ├── whoami.go │ └── whoami_test.go ├── clouds │ ├── aws │ │ ├── common.go │ │ ├── ecs │ │ │ ├── cfn │ │ │ │ ├── outputs │ │ │ │ │ └── ids.go │ │ │ │ ├── setup.go │ │ │ │ ├── setup_test.go │ │ │ │ ├── template.go │ │ │ │ ├── template_test.go │ │ │ │ └── waiter.go │ │ │ ├── common.go │ │ │ ├── common_test.go │ │ │ ├── ecsserviceaction │ │ │ │ ├── aws_event.go │ │ │ │ ├── ecs_service_action.go │ │ │ │ └── marshaller.go │ │ │ ├── ecstaskstatechange │ │ │ │ ├── attachment_details.go │ │ │ │ ├── attachment_details_details.go │ │ │ │ ├── attributes_details.go │ │ │ │ ├── aws_event.go │ │ │ │ ├── container_details.go │ │ │ │ ├── ecs_task_state_change.go │ │ │ │ ├── environment.go │ │ │ │ ├── marshaller.go │ │ │ │ ├── network_binding_details.go │ │ │ │ ├── network_interface_details.go │ │ │ │ ├── overrides.go │ │ │ │ └── overrides_item.go │ │ │ ├── event.go │ │ │ ├── fargate.go │ │ │ ├── fargate_test.go │ │ │ ├── info.go │ │ │ ├── logs.go │ │ │ ├── logs_test.go │ │ │ ├── merge.go │ │ │ ├── run.go │ │ │ ├── status.go │ │ │ ├── stop.go │ │ │ ├── stream.go │ │ │ ├── stream_test.go │ │ │ ├── tail.go │ │ │ ├── tail_test.go │ │ │ └── upload.go │ │ ├── region │ │ │ └── region.go │ │ ├── route53.go │ │ ├── route53_test.go │ │ ├── s3.go │ │ ├── secrets.go │ │ └── secrets_test.go │ ├── do │ │ ├── appPlatform │ │ │ ├── parse.go │ │ │ ├── parse_test.go │ │ │ ├── setup.go │ │ │ └── setup_test.go │ │ ├── common.go │ │ └── region │ │ │ └── region.go │ └── gcp │ │ ├── account.go │ │ ├── account_test.go │ │ ├── api.go │ │ ├── artifactregistry.go │ │ ├── cloudbuild.go │ │ ├── cloudrun.go │ │ ├── dns.go │ │ ├── iam.go │ │ ├── logging.go │ │ ├── project.go │ │ ├── secret.go │ │ └── storage.go ├── cmd │ ├── common.go │ ├── common_test.go │ ├── destroy.go │ ├── factory.go │ ├── info.go │ ├── logs.go │ ├── run.go │ └── stop.go ├── dns │ ├── check.go │ ├── check_test.go │ ├── mock.go │ ├── resolver.go │ ├── resolver_integration_test.go │ ├── resolver_test.go │ ├── utils.go │ └── utils_test.go ├── docker │ ├── common.go │ ├── info.go │ ├── run.go │ ├── run_test.go │ ├── setup.go │ ├── stop.go │ ├── tail.go │ └── teardown.go ├── github │ ├── id_token.go │ └── id_token_test.go ├── http │ ├── client.go │ ├── get.go │ ├── post.go │ ├── post_test.go │ ├── put.go │ ├── put_test.go │ ├── query.go │ └── query_test.go ├── local │ ├── local.go │ └── local_test.go ├── logs │ ├── fluent.go │ ├── log_type.go │ ├── log_type_test.go │ ├── logrus.go │ ├── logrus_test.go │ └── slog.go ├── mcp │ ├── README.md │ ├── deployment_info │ │ └── deployment_info.go │ ├── resources │ │ └── resources.go │ ├── setup.go │ ├── tools │ │ ├── common.go │ │ ├── deploy.go │ │ ├── destroy.go │ │ ├── login.go │ │ ├── services.go │ │ └── tools.go │ └── utils.go ├── money │ ├── money.go │ └── money_test.go ├── quota │ ├── quota.go │ ├── service.go │ └── service_test.go ├── scope │ └── scope.go ├── spinner │ └── spinner.go ├── term │ ├── LICENSE │ ├── colorizer.go │ ├── colorizer_other.go │ ├── colorizer_test.go │ ├── colorizer_windows.go │ ├── input.go │ ├── table.go │ ├── test_utils.go │ ├── unbuf.go │ ├── unbuf_unix.go │ ├── unbuf_unix_bsd.go │ ├── unbuf_unix_other.go │ └── unbuf_windows.go ├── track │ └── track.go ├── types │ ├── driver.go │ ├── error.go │ ├── etag.go │ └── tenant.go ├── utils.go └── utils_test.go ├── protos ├── buf.yaml ├── google │ └── type │ │ ├── money.pb.go │ │ └── money.proto └── io │ └── defang │ └── v1 │ ├── defangv1connect │ └── fabric.connect.go │ ├── fabric.pb.go │ └── fabric.proto └── testdata ├── .gitignore ├── Fancy-Proj_Dir ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── alttestproj ├── Dockerfile ├── altcomp.yaml ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden ├── compose.yaml.warnings └── ignored.Dockerfile ├── build ├── Dockerfile ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── compose-go-warn ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── configdetection ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── configoverride ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── debugproj ├── Dockerfile ├── app │ ├── Dockerfile │ └── main.js ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden ├── compose.yaml.warnings └── main.py ├── dependson ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── empty ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── emptyenv ├── Dockerfile ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── fixupenv ├── Dockerfile ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── gpu ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── healthcheck ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── interpolate ├── compose.dev.yaml ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── llm ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── longname ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── multiple ├── compose1.yaml └── compose2.yaml ├── networks ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── noprojname ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── platforms ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── ports ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── postgres ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── profiles ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── provider ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── redis ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── sanity ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── secretname ├── Dockerfile ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── static-files ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden └── compose.yaml.warnings ├── testproj ├── .dockerignore ├── .env ├── Dockerfile ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden ├── compose.yaml.warnings └── fileName.env └── toomany ├── compose.yaml ├── compose.yaml.fixup ├── compose.yaml.golden ├── compose.yaml.warnings ├── docker-compose.yml ├── docker-compose.yml.fixup ├── docker-compose.yml.golden └── docker-compose.yml.warnings /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Defang CLI Dev Container", 3 | "image": "ubuntu:latest", 4 | "features": { 5 | "ghcr.io/devcontainers/features/nix:1": { 6 | "extraNixConfig": "experimental-features = nix-command flakes" 7 | } 8 | }, 9 | "containerEnv": { 10 | "EDITOR": "vim" 11 | }, 12 | "customizations": { 13 | "vscode": { 14 | "extensions": [ 15 | "golang.go", 16 | "ms-vscode.makefile-tools" 17 | ] 18 | } 19 | }, 20 | "postAttachCommand": "nix develop --command bash && make setup" 21 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description: 2 | 3 | 4 | 5 | 6 | ## Steps to reproduce the problem. 7 | 8 | ## Expected behavior. 9 | 10 | ## Actual behavior. 11 | 12 | ## Environment: 13 | 14 | - Defang Version (`defang version`): 15 | - Operating system: (e.g., Windows, macOS, Linux) 16 | - Shell (or Browser, if applicable): 17 | 18 | ## Additional Information, Relevant dependencies, Screenshots, Logs, Etc... 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | ## Linked Issues 6 | 7 | 8 | 9 | ## Checklist 10 | 11 | - [ ] I have performed a self-review of my code 12 | - [ ] I have added appropriate tests 13 | - [ ] I have updated the Defang CLI docs and/or README to reflect my changes, if necessary 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/build-samples-json.yml: -------------------------------------------------------------------------------- 1 | name: Trigger Docs Samples Rebuild 2 | 3 | on: 4 | push: 5 | branches: main 6 | paths: 7 | - 'samples/**' 8 | 9 | jobs: 10 | build-json: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Trigger CLI Autodoc 14 | uses: peter-evans/repository-dispatch@v3 15 | with: 16 | token: ${{ secrets.DOCS_ACTION_TRIGGER_TOKEN }} 17 | repository: DefangLabs/defang-docs 18 | event-type: sample-update 19 | -------------------------------------------------------------------------------- /.github/workflows/install.yml: -------------------------------------------------------------------------------- 1 | name: Install script 2 | 3 | on: 4 | release: 5 | types: [published] 6 | push: 7 | branches: 8 | - "**" 9 | paths: 10 | - '.github/workflows/install.yml' 11 | - 'src/bin/install' 12 | 13 | jobs: 14 | install: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Install defang (latest) 19 | run: eval "$(curl -fsSL s.defang.io/install)" 20 | env: 21 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # avoid rate limiting 22 | 23 | - name: Sanity check 24 | run: defang --version 25 | 26 | - name: Install defang (specific version) 27 | run: eval "$(curl -fsSL s.defang.io/install)" 28 | env: 29 | DEFANG_INSTALL_VERSION: v0.5.36 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # alt name 31 | 32 | - name: Sanity check 33 | run: defang --version 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .envrc 3 | .direnv/ 4 | .DS_Store 5 | /result 6 | .idea 7 | 8 | .bin/ 9 | 10 | *.log 11 | *.txt 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "github.vscode-pull-request-github", 4 | "wayou.vscode-todo-highlight" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.lintTool": "golangci-lint" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Defang Software Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install-git-hooks 2 | install-git-hooks: 3 | printf "#!/bin/sh\nmake pre-commit" > .git/hooks/pre-commit 4 | chmod +x .git/hooks/pre-commit 5 | printf "#!/bin/sh\nmake pre-push" > .git/hooks/pre-push 6 | chmod +x .git/hooks/pre-push 7 | 8 | .PHONY: pre-commit 9 | pre-commit: 10 | @if git diff --cached --name-only | grep -q '^src/'; then make -C src lint; fi 11 | 12 | .PHONY: pre-push 13 | pre-push: 14 | @make -C src test 15 | 16 | setup: 17 | go -C src mod tidy 18 | -------------------------------------------------------------------------------- /defang.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | }, 6 | { 7 | "path": "src" 8 | } 9 | ], 10 | "settings": { 11 | "go.testEnvVars": { 12 | "AWS_PROFILE": "defang-sandbox" 13 | }, 14 | "go.buildTags": "integration", 15 | "go.testFlags": ["-short"], 16 | "go.testTimeout": "300s", 17 | "makefile.configureOnOpen": false 18 | } 19 | } -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import { }, 3 | }: 4 | pkgs.callPackage ./pkgs/defang/cli.nix { } 5 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "id": "flake-utils", 17 | "type": "indirect" 18 | } 19 | }, 20 | "nixpkgs": { 21 | "locked": { 22 | "lastModified": 1747467164, 23 | "narHash": "sha256-JBXbjJ0t6T6BbVc9iPVquQI9XSXCGQJD8c8SgnUquus=", 24 | "owner": "NixOS", 25 | "repo": "nixpkgs", 26 | "rev": "3fcbdcfc707e0aa42c541b7743e05820472bdaec", 27 | "type": "github" 28 | }, 29 | "original": { 30 | "id": "nixpkgs", 31 | "type": "indirect" 32 | } 33 | }, 34 | "root": { 35 | "inputs": { 36 | "flake-utils": "flake-utils", 37 | "nixpkgs": "nixpkgs" 38 | } 39 | }, 40 | "systems": { 41 | "locked": { 42 | "lastModified": 1681028828, 43 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 44 | "owner": "nix-systems", 45 | "repo": "default", 46 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "nix-systems", 51 | "repo": "default", 52 | "type": "github" 53 | } 54 | } 55 | }, 56 | "root": "root", 57 | "version": 7 58 | } 59 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | outputs = 3 | { 4 | self, 5 | nixpkgs, 6 | flake-utils, 7 | }: 8 | flake-utils.lib.eachDefaultSystem ( 9 | system: 10 | let 11 | pkgs = import nixpkgs { 12 | inherit system; 13 | }; 14 | in 15 | { 16 | devShell = 17 | with pkgs; 18 | mkShell { 19 | buildInputs = 20 | [ 21 | buf 22 | crane 23 | git 24 | gnumake 25 | less 26 | gnused # force Linux `sed` everywhere 27 | go_1_23 28 | golangci-lint 29 | goreleaser 30 | nixfmt-rfc-style 31 | nodejs_20 # for Pulumi, must match values in package.json 32 | openssh 33 | pulumi-bin 34 | google-cloud-sdk 35 | vim 36 | ]; 37 | }; 38 | packages.defang-cli = pkgs.callPackage ./pkgs/defang/cli.nix { }; 39 | packages.defang-bin = pkgs.callPackage ./pkgs/defang { }; 40 | } 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /pkgs/defang/cli.nix: -------------------------------------------------------------------------------- 1 | { 2 | buildGoModule, 3 | installShellFiles, 4 | lib, 5 | }: 6 | buildGoModule { 7 | pname = "defang-cli"; 8 | version = "git"; 9 | src = ../../src; 10 | vendorHash = "sha256-1f9SZOOr60sgK2QAaf0F7Q6a2Biq2JVlV7BTSjC0Hus="; # TODO: use fetchFromGitHub 11 | 12 | subPackages = [ "cmd/cli" ]; 13 | 14 | nativeBuildInputs = [ installShellFiles ]; 15 | 16 | env.CGO_ENABLED = 0; 17 | ldflags = [ 18 | "-s" 19 | "-w" 20 | ]; 21 | doCheck = false; # some unit tests need internet access 22 | 23 | postInstall = '' 24 | mv $out/bin/cli $out/bin/defang 25 | installShellCompletion --cmd defang \ 26 | --bash <($out/bin/defang completion bash) \ 27 | --zsh <($out/bin/defang completion zsh) \ 28 | --fish <($out/bin/defang completion fish) 29 | ''; 30 | 31 | meta = with lib; { 32 | description = "CLI to take your app from Docker Compose to a secure and scalable deployment on your favorite cloud in minutes"; 33 | homepage = "https://defang.io/"; 34 | license = licenses.mit; 35 | maintainers = with maintainers; [ lionello ]; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /pkgs/npm/.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | -------------------------------------------------------------------------------- /pkgs/npm/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.* 2 | *.ts 3 | test/ 4 | .vscode/ 5 | 6 | -------------------------------------------------------------------------------- /pkgs/npm/mocha.ts: -------------------------------------------------------------------------------- 1 | import Mocha from 'mocha'; 2 | import path from 'path'; 3 | import fs from 'fs/promises'; 4 | 5 | async function main() { 6 | try { 7 | const mocha = new Mocha({ 8 | // @ts-expect-error: MochaOptions doesn't type this but it's valid 9 | extension: ['ts'] 10 | }); 11 | 12 | const testDir = new URL('./test', import.meta.url); 13 | const entries = await fs.readdir(testDir.pathname); 14 | 15 | for (const file of entries) { 16 | if (file.endsWith('.spec.ts')) { 17 | const fullPath = path.resolve(testDir.pathname, file); 18 | mocha.addFile(fullPath); 19 | } 20 | } 21 | 22 | await mocha.loadFilesAsync(); 23 | 24 | mocha.run(failures => { 25 | process.exitCode = failures ? 1 : 0; 26 | }); 27 | } catch (err) { 28 | console.error("error in main():", err); 29 | process.exit(1); 30 | } 31 | } 32 | 33 | main(); -------------------------------------------------------------------------------- /pkgs/npm/src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { run } from "./clilib.js"; 4 | 5 | run(); 6 | -------------------------------------------------------------------------------- /pkgs/npm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "declaration": false, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "isolatedModules": true, 10 | "lib": ["es2021"], 11 | "module": "ESNext", 12 | "moduleResolution": "node", 13 | "noEmit": false, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitAny": true, 16 | "noImplicitReturns": true, 17 | "outDir": "bin", 18 | "pretty": true, 19 | "removeComments": true, 20 | "resolveJsonModule": true, 21 | "skipLibCheck": true, 22 | "sourceMap": false, 23 | "strict": true, 24 | "stripInternal": true, 25 | "target": "ES2021", 26 | "typeRoots": ["../node_modules/@types"] 27 | }, 28 | "include": ["src/**/*.ts"], 29 | "exclude": ["node_modules", "**/*.spec.ts"] 30 | } 31 | -------------------------------------------------------------------------------- /pkgs/npm/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowImportingTsExtensions": true, 4 | "allowJs": true, 5 | "allowSyntheticDefaultImports": true, 6 | "declaration": false, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "isolatedModules": true, 11 | "lib": ["es2021"], 12 | "module": "ESNext", 13 | "moduleResolution": "node", 14 | "noEmit": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "outDir": "bin", 19 | "pretty": true, 20 | "removeComments": true, 21 | "resolveJsonModule": true, 22 | "skipLibCheck": true, 23 | "sourceMap": false, 24 | "strict": true, 25 | "stripInternal": true, 26 | "target": "ES2021", 27 | "typeRoots": ["../node_modules/@types"] 28 | }, 29 | "include": ["src/**/*.ts"], 30 | "exclude": ["node_modules", "**/*.spec.ts"] 31 | } 32 | -------------------------------------------------------------------------------- /src/.dockerignore: -------------------------------------------------------------------------------- 1 | # ignore everything 2 | * 3 | 4 | # we only need these files to build the binary 5 | !/cmd 6 | !/pkg 7 | !/protos 8 | !go.sum 9 | !go.mod 10 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | /defang 2 | /cli 3 | /server 4 | defang_linux_amd64.zip 5 | defang_darwin.zip 6 | generateEngine_test.log 7 | defang-amd64 8 | defang-arm64 9 | dist/ 10 | distx/ 11 | defang.exe 12 | samples_examples.json 13 | knowledge_base.json 14 | -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | ARG GOVERSION=1.23 3 | FROM --platform=${BUILDPLATFORM} golang:${GOVERSION} AS builder 4 | 5 | # These two are automatically set by docker buildx 6 | ARG TARGETARCH 7 | ARG TARGETOS 8 | 9 | # Set the working directory 10 | WORKDIR /app 11 | 12 | COPY --link go.mod go.sum ./ 13 | RUN go mod download 14 | 15 | # Copy source code 16 | COPY . /app/ 17 | 18 | # Build the application 19 | ARG BUILD=./cmd/cli 20 | ARG VERSION 21 | ARG OUTPUT=defang 22 | RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -buildvcs=false -ldflags="-w -s -X \"main.version=${VERSION}\"" -o "${OUTPUT}" "${BUILD}" 23 | 24 | # Final stage 25 | FROM alpine:latest 26 | 27 | # Install runtime dependencies 28 | RUN apk add --no-cache ca-certificates tzdata 29 | 30 | # Create a non-root user to run the application 31 | RUN adduser -D -h /home/defang defang 32 | 33 | # Set working directory 34 | WORKDIR /app 35 | 36 | # Copy the binary from the builder stage 37 | COPY --from=builder /app/defang /usr/local/bin/ 38 | 39 | # Set the user to run the application 40 | USER defang 41 | 42 | # For the authentication server 43 | EXPOSE 47071 44 | 45 | ENTRYPOINT ["defang"] 46 | 47 | CMD ["--help"] 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | ## Build 2 | ``` 3 | go mod download 4 | make 5 | ``` 6 | 7 | ## Run 8 | ``` 9 | ./defang 10 | ``` 11 | 12 | ## Format Code 13 | ``` 14 | go fmt 15 | ``` 16 | 17 | ## Update Dependencies 18 | To regenerate the `go.mod` file: 19 | ``` 20 | go mod tidy 21 | ``` 22 | 23 | ## Release 24 | To release a new version, run: 25 | ``` 26 | make release 27 | ``` 28 | This will create a new tag (incrementing the patch number) and push it to the 29 | repository, triggering a new build on the CI/CD pipeline. 30 | -------------------------------------------------------------------------------- /src/bin/completions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Generate all shell completions for defang 3 | # Usage: ./completions.sh [path/to/defang] 4 | 5 | set -e # Exit on error 6 | 7 | OUTPUT_DIR=$PWD 8 | SCRIPT_DIR=`dirname "$0"` 9 | DEFANG=$1 10 | 11 | # If no path to the defang CLI is provided, use Go to build and run it 12 | if [ -z "$DEFANG" ]; then 13 | DEFANG="go run ./cmd/cli/main.go" 14 | cd "$SCRIPT_DIR/.." 15 | fi 16 | 17 | for sh in bash zsh fish powershell; do 18 | $DEFANG completion "$sh" >"$OUTPUT_DIR/defang.$sh" 19 | done 20 | -------------------------------------------------------------------------------- /src/bin/notarize.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # Bail if we didn't get one (and only one) argument 5 | if [ $# -ne 1 ]; then 6 | echo "Usage: $0 " 7 | exit 1 8 | fi 9 | 10 | if [ -z "$MACOS_NOTARIZATION_APPLE_ID" ]; then 11 | echo "Error: missing env var MACOS_NOTARIZATION_APPLE_ID" 12 | exit 2 13 | fi 14 | 15 | if [ -z "$MACOS_NOTARIZATION_TEAM_ID" ]; then 16 | echo "Error: missing env var MACOS_NOTARIZATION_TEAM_ID" 17 | exit 3 18 | fi 19 | 20 | if [ -z "$MACOS_NOTARIZATION_APP_PW" ]; then 21 | echo "Error: missing env var MACOS_NOTARIZATION_APP_PW" 22 | exit 4 23 | fi 24 | 25 | [ "$ACTIONS_STEP_DEBUG" = 'true' ] || [ "$DEBUG" = 'true' ] && set -x 26 | 27 | xcrun notarytool submit "$1" --apple-id "$MACOS_NOTARIZATION_APPLE_ID" --team-id "$MACOS_NOTARIZATION_TEAM_ID" --password "$MACOS_NOTARIZATION_APP_PW" 28 | -------------------------------------------------------------------------------- /src/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | managed: 3 | enabled: true 4 | go_package_prefix: 5 | default: github.com/DefangLabs/defang/src/protos 6 | plugins: 7 | - plugin: buf.build/protocolbuffers/go:v1.36.3 8 | out: protos 9 | opt: paths=source_relative 10 | - plugin: buf.build/bufbuild/connect-go 11 | out: protos 12 | opt: paths=source_relative 13 | -------------------------------------------------------------------------------- /src/cmd/cli/command/color.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import "fmt" 4 | 5 | type ColorMode string 6 | 7 | const ( 8 | // ColorNever disables color output. 9 | ColorNever ColorMode = "never" 10 | // ColorAuto enables color output only if the output is connected to a terminal. 11 | ColorAuto ColorMode = "auto" 12 | // ColorAlways enables color output. 13 | ColorAlways ColorMode = "always" 14 | // ColorRaw disables color output and does not escape any characters. 15 | // ColorRaw ColorMode = "raw" 16 | ) 17 | 18 | var allColorModes = []ColorMode{ 19 | ColorNever, 20 | ColorAuto, 21 | ColorAlways, 22 | } 23 | 24 | func (c ColorMode) String() string { 25 | return string(c) 26 | } 27 | 28 | func (c *ColorMode) Set(value string) error { 29 | for _, colorMode := range allColorModes { 30 | if colorMode.String() == value { 31 | *c = colorMode 32 | return nil 33 | } 34 | } 35 | return fmt.Errorf("color mode not one of %v", allColorModes) 36 | } 37 | 38 | func (c ColorMode) Type() string { 39 | return "color-mode" 40 | } 41 | -------------------------------------------------------------------------------- /src/cmd/cli/command/compose_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestInitializeTailCmd(t *testing.T) { 8 | t.Run("", func(t *testing.T) { 9 | for _, cmd := range RootCmd.Commands() { 10 | if cmd.Use == "logs" { 11 | cmd.Execute() 12 | return 13 | } 14 | } 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/cmd/cli/command/configrandom.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "regexp" 7 | ) 8 | 9 | func CreateRandomConfigValue() string { 10 | // Note that no error handling is necessary, as Read always succeeds. 11 | key := make([]byte, 32) 12 | rand.Read(key) 13 | str := base64.StdEncoding.EncodeToString(key) 14 | re := regexp.MustCompile("[+/=]") 15 | str = re.ReplaceAllString(str, "") 16 | return str 17 | } 18 | -------------------------------------------------------------------------------- /src/cmd/cli/command/exitcode.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import "fmt" 4 | 5 | type ExitCode int 6 | 7 | func (e ExitCode) Error() string { 8 | return fmt.Sprintf("exit code %d", e) 9 | } 10 | -------------------------------------------------------------------------------- /src/cmd/cli/command/mode.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "strings" 7 | 8 | defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" 9 | ) 10 | 11 | type Mode defangv1.DeploymentMode 12 | 13 | func (b Mode) String() string { 14 | if b == 0 { 15 | return "" 16 | } 17 | return strings.ToLower(defangv1.DeploymentMode_name[int32(b)]) 18 | } 19 | 20 | func (b *Mode) Set(s string) error { 21 | mode, ok := defangv1.DeploymentMode_value[strings.ToUpper(s)] 22 | if !ok { 23 | return fmt.Errorf("invalid mode: %s, not one of %v", s, allModes()) 24 | } 25 | *b = Mode(mode) 26 | return nil 27 | } 28 | 29 | func (b Mode) Type() string { 30 | return "mode" 31 | } 32 | 33 | func (b Mode) Value() defangv1.DeploymentMode { 34 | return defangv1.DeploymentMode(b) 35 | } 36 | 37 | func allModes() []string { 38 | modes := make([]string, 0, len(defangv1.DeploymentMode_name)-1) 39 | for i, mode := range defangv1.DeploymentMode_name { 40 | if i == 0 { 41 | continue 42 | } 43 | modes = append(modes, strings.ToLower(mode)) 44 | } 45 | slices.Sort(modes) // TODO: sort by enum value instead of string 46 | return modes 47 | } 48 | -------------------------------------------------------------------------------- /src/cmd/cli/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestSkipLines(t *testing.T) { 9 | buf := []byte(`goroutine 1 [running]: 10 | main.main.func1() 11 | /Users/user/dev/defang/src/cmd/cli/main.go:18 +0x100 12 | panic({0x105f35200?, 0x10670b530?}) 13 | /nix/store/v8llgr5prc0rawmgynacggg0q4pbvk5w-go-1.21.10/share/go/src/runtime/panic.go:914 +0x218 14 | github.com/DefangLabs/defang/src/pkg/clouds/do/appPlatform.newClient({0x106729288, 0x10743e240}) 15 | /Users/user/dev/defang/src/pkg/clouds/do/appPlatform/setup.go:224 +0xb0`) 16 | 17 | expected := []byte(`github.com/DefangLabs/defang/src/pkg/clouds/do/appPlatform.newClient({0x106729288, 0x10743e240}) 18 | /Users/user/dev/defang/src/pkg/clouds/do/appPlatform/setup.go:224 +0xb0`) 19 | 20 | actual := skipLines(buf, 6) 21 | if !bytes.Equal(expected, actual) { 22 | t.Errorf("Expected %q, got %q", expected, actual) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/cmd/cli/setuidgid_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | ) 9 | 10 | func setUidGidFromFile(path string) error { 11 | // Find out the owner of the given path 12 | stat, err := os.Stat(path) 13 | if err != nil { 14 | return err 15 | } 16 | if statt, ok := stat.Sys().(*syscall.Stat_t); ok { 17 | syscall.Setgid(int(statt.Gid)) 18 | return syscall.Setuid(int(statt.Uid)) 19 | } 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /src/cmd/cli/setuidgid_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package main 4 | 5 | func setUidGidFromFile(path string) error { 6 | // Windows does not support changing file ownership 7 | return nil 8 | } 9 | -------------------------------------------------------------------------------- /src/cmd/cli/version.go: -------------------------------------------------------------------------------- 1 | package main // this must be "main" or -ldflags will fail to set the version 2 | 3 | var version = "development" // overwritten by build script -ldflags "-X main.version=..." and GoReleaser 4 | -------------------------------------------------------------------------------- /src/cmd/gendocs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/DefangLabs/defang/src/cmd/cli/command" 8 | "github.com/DefangLabs/defang/src/pkg/term" 9 | "github.com/spf13/cobra/doc" 10 | ) 11 | 12 | // runs in docs ci to generate markdown docs 13 | func main() { 14 | if len(os.Args) < 2 { 15 | panic("Missing required argument: docs path") 16 | } 17 | 18 | docsPath := os.Args[1] 19 | 20 | _ = os.Mkdir(docsPath, 0755) 21 | 22 | command.SetupCommands(context.Background(), "") 23 | 24 | err := doc.GenMarkdownTree(command.RootCmd, docsPath) 25 | if err != nil { 26 | term.Fatal(err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/pkg/auth/interceptor.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/bufbuild/connect-go" 8 | ) 9 | 10 | const XDefangOrgID = "x-defang-orgid" 11 | 12 | type authInterceptor struct { 13 | authorization string 14 | orgID string 15 | } 16 | 17 | func NewAuthInterceptor(token, orgID string) connect.Interceptor { 18 | return &authInterceptor{"Bearer " + strings.TrimSpace(token), orgID} 19 | } 20 | 21 | func (a *authInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { 22 | return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { 23 | req.Header().Set("authorization", a.authorization) 24 | req.Header().Set("content-type", "application/grpc") // same as the gRPC client 25 | req.Header().Set(XDefangOrgID, a.orgID) 26 | return next(ctx, req) 27 | } 28 | } 29 | 30 | func (a *authInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { 31 | return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { 32 | conn := next(ctx, spec) 33 | conn.RequestHeader().Set("authorization", a.authorization) 34 | conn.RequestHeader().Set("content-type", "application/grpc") // same as the gRPC client 35 | conn.RequestHeader().Set(XDefangOrgID, a.orgID) 36 | return conn 37 | } 38 | } 39 | 40 | func (authInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { 41 | return next 42 | } 43 | -------------------------------------------------------------------------------- /src/pkg/auth/pkce.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "errors" 8 | ) 9 | 10 | type Method string 11 | 12 | const ( 13 | PlainMethod Method = "plain" 14 | S256Method Method = "S256" 15 | ) 16 | 17 | func generateVerifier(length int) (string, error) { 18 | buffer := make([]byte, length) 19 | if _, err := rand.Read(buffer); err != nil { 20 | return "", err 21 | } 22 | return base64.RawURLEncoding.EncodeToString(buffer), nil 23 | } 24 | 25 | func generateChallenge(verifier string, method Method) string { 26 | if method == PlainMethod { 27 | return verifier 28 | } 29 | hash := sha256.Sum256([]byte(verifier)) 30 | return base64.RawURLEncoding.EncodeToString(hash[:]) 31 | } 32 | 33 | type PKCE struct { 34 | Verifier string 35 | Challenge string 36 | Method 37 | } 38 | 39 | func GeneratePKCE(length int) (PKCE, error) { 40 | if length < 43 || length > 128 { 41 | return PKCE{}, errors.New( 42 | "code verifier length must be between 43 and 128 characters", 43 | ) 44 | } 45 | verifier, err := generateVerifier(length) 46 | if err != nil { 47 | return PKCE{}, err 48 | } 49 | const method = S256Method 50 | challenge := generateChallenge(verifier, method) 51 | return PKCE{ 52 | Verifier: verifier, 53 | Challenge: challenge, 54 | Method: method, 55 | }, nil 56 | } 57 | 58 | func ValidatePKCE( 59 | verifier string, 60 | challenge string, 61 | method Method, 62 | ) bool { 63 | generatedChallenge := generateChallenge(verifier, method) 64 | // timing safe equals? 65 | return generatedChallenge == challenge 66 | } 67 | -------------------------------------------------------------------------------- /src/pkg/cert/check.go: -------------------------------------------------------------------------------- 1 | package cert 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/DefangLabs/defang/src/pkg/dns" 10 | ) 11 | 12 | func CheckTLSCert(ctx context.Context, domain string) error { 13 | resolver := dns.RootResolver{} 14 | ips, err := resolver.LookupIPAddr(ctx, domain) 15 | if err != nil { 16 | return err 17 | } 18 | for _, ip := range ips { 19 | url := "https://" + domain 20 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 21 | if err != nil { 22 | return err 23 | } 24 | httpClient := &http.Client{ 25 | Transport: getFixedIPTransport(ip.String()), 26 | } 27 | resp, err := httpClient.Do(req) 28 | if err != nil { 29 | return err 30 | } 31 | defer resp.Body.Close() 32 | } 33 | return nil 34 | } 35 | 36 | func getFixedIPTransport(ip string) *http.Transport { 37 | return &http.Transport{ 38 | Proxy: http.ProxyFromEnvironment, 39 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 40 | _, port, err := net.SplitHostPort(addr) 41 | if err != nil { 42 | return nil, err 43 | } 44 | dialer := &net.Dialer{} 45 | rootAddr := net.JoinHostPort(ip, port) 46 | return dialer.DialContext(ctx, network, rootAddr) 47 | }, 48 | ForceAttemptHTTP2: true, 49 | MaxIdleConns: 100, 50 | IdleConnTimeout: 90 * time.Second, 51 | TLSHandshakeTimeout: 10 * time.Second, 52 | ExpectContinueTimeout: 1 * time.Second, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/pkg/cli/client/byoc/aws/domain_integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package aws 4 | 5 | import ( 6 | "context" 7 | "testing" 8 | 9 | "github.com/aws/aws-sdk-go-v2/config" 10 | "github.com/aws/aws-sdk-go-v2/service/route53" 11 | ) 12 | 13 | func TestPrepareDomainDelegation(t *testing.T) { 14 | ctx := context.Background() 15 | cfg, err := config.LoadDefaultConfig(ctx) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | r53Client := route53.NewFromConfig(cfg) 21 | 22 | testPrepareDomainDelegationNew(t, r53Client) 23 | testPrepareDomainDelegationLegacy(t, r53Client) 24 | } 25 | -------------------------------------------------------------------------------- /src/pkg/cli/client/byoc/aws/subscribe.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "slices" 5 | "sync/atomic" 6 | 7 | "github.com/DefangLabs/defang/src/pkg/clouds/aws/ecs" 8 | "github.com/DefangLabs/defang/src/pkg/types" 9 | defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" 10 | ) 11 | 12 | type byocSubscribeServerStream struct { 13 | services []string 14 | etag types.ETag 15 | 16 | ch chan *defangv1.SubscribeResponse 17 | resp *defangv1.SubscribeResponse 18 | err error 19 | closed atomic.Bool 20 | } 21 | 22 | func (s *byocSubscribeServerStream) HandleECSEvent(evt ecs.Event) { 23 | if etag := evt.Etag(); etag == "" || etag != s.etag { 24 | return 25 | } 26 | if service := evt.Service(); len(s.services) > 0 && !slices.Contains(s.services, service) { 27 | return 28 | } 29 | s.send(&defangv1.SubscribeResponse{ 30 | Name: evt.Service(), 31 | Status: evt.Status(), 32 | State: evt.State(), 33 | }) 34 | } 35 | 36 | func (s *byocSubscribeServerStream) Close() error { 37 | s.closed.Store(true) 38 | close(s.ch) 39 | return nil 40 | } 41 | 42 | func (s *byocSubscribeServerStream) Receive() bool { 43 | resp, ok := <-s.ch 44 | if !ok || resp == nil { 45 | return false 46 | } 47 | s.resp = resp 48 | return true 49 | } 50 | 51 | func (s *byocSubscribeServerStream) Msg() *defangv1.SubscribeResponse { 52 | return s.resp 53 | } 54 | 55 | func (s *byocSubscribeServerStream) Err() error { 56 | return s.err 57 | } 58 | 59 | func (s *byocSubscribeServerStream) send(resp *defangv1.SubscribeResponse) { 60 | if s.closed.Load() { 61 | return 62 | } 63 | s.ch <- resp 64 | } 65 | -------------------------------------------------------------------------------- /src/pkg/cli/client/byoc/aws/testdata/build-failure.events: -------------------------------------------------------------------------------- 1 | {"name":"web","status":"BUILD_PROVISIONING ","state":2} 2 | {"name":"web","status":"BUILD_PENDING ","state":3} 3 | {"name":"web","status":"BUILD_RUNNING ","state":5} 4 | {"name":"api","status":"TASK_PROVISIONING","state":8} 5 | {"name":"api","status":"TASK_PENDING","state":8} 6 | {"name":"api","status":"TASK_PENDING","state":8} 7 | {"name":"api","status":"TASK_ACTIVATING","state":8} 8 | {"name":"api","status":"TASK_RUNNING","state":8} 9 | {"name":"web","status":"BUILD_DEPROVISIONING Essential container in task exited","state":11} 10 | {"name":"api","status":"SERVICE_DEPLOYMENT_COMPLETED ECS deployment ecs-svc/9592482416091746279 completed.","state":9} 11 | -------------------------------------------------------------------------------- /src/pkg/cli/client/byoc/aws/testdata/failure-then-success.events: -------------------------------------------------------------------------------- 1 | {"name":"web","status":"BUILD_PROVISIONING ","state":2} 2 | {"name":"web","status":"BUILD_PENDING ","state":3} 3 | {"name":"web","status":"BUILD_RUNNING ","state":5} 4 | {"name":"web","status":"BUILD_DEPROVISIONING Essential container in task exited","state":6} 5 | {"name":"web","status":"BUILD_PROVISIONING ","state":2} 6 | {"name":"web","status":"BUILD_PENDING ","state":3} 7 | {"name":"api","status":"TASK_PROVISIONING","state":8} 8 | {"name":"api","status":"TASK_PENDING","state":8} 9 | {"name":"web","status":"BUILD_RUNNING ","state":5} 10 | {"name":"api","status":"TASK_PENDING","state":8} 11 | {"name":"api","status":"TASK_ACTIVATING","state":8} 12 | {"name":"api","status":"TASK_RUNNING","state":8} 13 | {"name":"api","status":"SERVICE_DEPLOYMENT_COMPLETED ECS deployment ecs-svc/1548079518837758163 completed.","state":9} 14 | {"name":"web","status":"BUILD_DEPROVISIONING Essential container in task exited","state":6} 15 | {"name":"web","status":"TASK_PROVISIONING","state":8} 16 | {"name":"web","status":"TASK_PENDING","state":8} 17 | {"name":"web","status":"TASK_PENDING","state":8} 18 | {"name":"web","status":"TASK_ACTIVATING","state":8} 19 | {"name":"web","status":"TASK_RUNNING","state":8} 20 | {"name":"web","status":"SERVICE_DEPLOYMENT_COMPLETED ECS deployment ecs-svc/0736160562103423478 completed.","state":9} 21 | -------------------------------------------------------------------------------- /src/pkg/cli/client/byoc/aws/testdata/success.events: -------------------------------------------------------------------------------- 1 | {"name":"api","status":"SERVICE_DEPLOYMENT_IN_PROGRESS ECS deployment ecs-svc/0548903365926546085 in progress.","state":8} 2 | {"name":"web","status":"SERVICE_DEPLOYMENT_IN_PROGRESS ECS deployment ecs-svc/8634367199868246568 in progress.","state":8} 3 | {"name":"web","status":"TASK_PROVISIONING","state":8} 4 | {"name":"api","status":"TASK_PROVISIONING","state":8} 5 | {"name":"web","status":"TASK_PENDING","state":8} 6 | {"name":"web","status":"TASK_PENDING","state":8} 7 | {"name":"web","status":"TASK_ACTIVATING","state":8} 8 | {"name":"web","status":"TASK_ACTIVATING","state":8} 9 | {"name":"api","status":"TASK_PENDING","state":8} 10 | {"name":"api","status":"TASK_PENDING","state":8} 11 | {"name":"web","status":"TASK_RUNNING","state":8} 12 | {"name":"api","status":"TASK_ACTIVATING","state":8} 13 | {"name":"api","status":"TASK_RUNNING","state":8} 14 | {"name":"web","status":"SERVICE_DEPLOYMENT_COMPLETED ECS deployment ecs-svc/8634367199868246568 completed.","state":9} 15 | {"name":"api","status":"SERVICE_DEPLOYMENT_COMPLETED ECS deployment ecs-svc/0548903365926546085 completed.","state":9} 16 | -------------------------------------------------------------------------------- /src/pkg/cli/client/byoc/do/config.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/digitalocean/godo" 7 | ) 8 | 9 | func deleteEnvVars(toDelete string, envVars *[]*godo.AppVariableDefinition) { 10 | var finalVars []*godo.AppVariableDefinition 11 | 12 | for _, envVar := range *envVars { 13 | if !strings.Contains(toDelete, envVar.Key) { 14 | finalVars = append(finalVars, envVar) 15 | } 16 | } 17 | *envVars = finalVars 18 | } 19 | -------------------------------------------------------------------------------- /src/pkg/cli/client/byoc/gcp/byoc_test.go: -------------------------------------------------------------------------------- 1 | package gcp 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "testing" 7 | ) 8 | 9 | func TestSetUpCD(t *testing.T) { 10 | t.Skip("skipping test") 11 | ctx := context.Background() 12 | b := NewByocProvider(ctx, "testTenantID") 13 | account, err := b.AccountInfo(ctx) 14 | if err != nil { 15 | t.Errorf("AccountInfo() error = %v, want nil", err) 16 | } 17 | t.Logf("account: %+v", account) 18 | if err := b.setUpCD(ctx); err != nil { 19 | t.Errorf("setUpCD() error = %v, want nil", err) 20 | } 21 | 22 | payload := base64.StdEncoding.EncodeToString([]byte(`services: 23 | nginx: 24 | image: nginx:1-alpine 25 | ports: 26 | - "8080:80" 27 | `)) 28 | cmd := cdCommand{ 29 | Project: "testproj", 30 | Command: []string{"up", payload}, 31 | } 32 | 33 | if op, err := b.runCdCommand(ctx, cmd); err != nil { 34 | t.Errorf("BootstrapCommand() error = %v, want nil", err) 35 | } else { 36 | t.Logf("BootstrapCommand() = %v", op) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/pkg/cli/client/errors.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "fmt" 4 | 5 | type ErrDeploymentFailed struct { 6 | Message string 7 | Service string // optional 8 | } 9 | 10 | func (e ErrDeploymentFailed) Error() string { 11 | var service string 12 | if e.Service != "" { 13 | service = fmt.Sprintf(" for service %q", e.Service) 14 | } 15 | return fmt.Sprintf("deployment failed%s: %s", service, e.Message) 16 | } 17 | -------------------------------------------------------------------------------- /src/pkg/cli/client/grpc_logger.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/DefangLabs/defang/src/pkg" 8 | "github.com/DefangLabs/defang/src/pkg/term" 9 | "github.com/bufbuild/connect-go" 10 | ) 11 | 12 | const maxPayloadLength = 1024 13 | 14 | type grpcLogger struct { 15 | prefix string 16 | } 17 | 18 | func (g grpcLogger) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { 19 | return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { 20 | // Add a request ID to the context 21 | requestId := pkg.RandomID() 22 | req.Header().Add("X-Request-Id", requestId) 23 | 24 | // Get the request type name 25 | reqType := req.Spec().Procedure 26 | 27 | // Convert request payload to JSON for logging 28 | payload, err := json.Marshal(req.Any()) 29 | if err != nil { 30 | payload = []byte("Error marshaling request payload") 31 | } 32 | 33 | // Truncate long payloads 34 | if len(payload) > maxPayloadLength { 35 | payload = append(payload[:maxPayloadLength], []byte("…")...) 36 | } 37 | 38 | term.Debug(g.prefix, requestId, reqType, string(payload)) 39 | return next(ctx, req) 40 | } 41 | } 42 | 43 | func (grpcLogger) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { 44 | return next 45 | } 46 | 47 | func (grpcLogger) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { 48 | return next 49 | } 50 | -------------------------------------------------------------------------------- /src/pkg/cli/client/playground_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestServiceDNS(t *testing.T) { 8 | p := PlaygroundProvider{FabricClient: GrpcClient{TenantName: "proj1"}} 9 | 10 | const expected = "proj1-service1" 11 | if got := p.ServiceDNS("service1"); got != expected { 12 | t.Errorf("ServiceDNS() = %v, want %v", got, expected) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/pkg/cli/client/projectName.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/DefangLabs/defang/src/pkg/term" 8 | ) 9 | 10 | func LoadProjectNameWithFallback(ctx context.Context, loader Loader, provider Provider) (string, error) { 11 | var loadErr error 12 | if loader != nil { 13 | projectName, err := loader.LoadProjectName(ctx) 14 | if err == nil { 15 | return projectName, nil 16 | } 17 | term.Debug("Failed to load local project:", err) 18 | loadErr = err 19 | } 20 | term.Debug("Trying to get the remote project name from the provider") 21 | projectName, err := provider.RemoteProjectName(ctx) 22 | if err != nil { 23 | return "", fmt.Errorf("%w and %w", loadErr, err) 24 | } 25 | return projectName, nil 26 | } 27 | -------------------------------------------------------------------------------- /src/pkg/cli/client/retrier.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/DefangLabs/defang/src/pkg" 8 | "github.com/bufbuild/connect-go" 9 | ) 10 | 11 | type Retrier struct{} 12 | 13 | func (Retrier) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { 14 | return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { 15 | res, err := next(ctx, req) 16 | if connect.CodeOf(err) == connect.CodeUnavailable { 17 | // Retry once after a 1 second sleep 18 | pkg.SleepWithContext(ctx, 1*time.Second) 19 | res, err = next(ctx, req) 20 | } 21 | return res, err 22 | } 23 | } 24 | func (Retrier) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { 25 | return next // TODO: wrap this to handle streaming rpcs like Tail 26 | } 27 | 28 | func (Retrier) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { 29 | return next 30 | } 31 | -------------------------------------------------------------------------------- /src/pkg/cli/client/retrier_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" 10 | "github.com/DefangLabs/defang/src/protos/io/defang/v1/defangv1connect" 11 | "github.com/bufbuild/connect-go" 12 | ) 13 | 14 | type grpcMockHandler struct { 15 | defangv1connect.UnimplementedFabricControllerHandler 16 | tries int 17 | } 18 | 19 | func (g *grpcMockHandler) Deploy(context.Context, *connect.Request[defangv1.DeployRequest]) (*connect.Response[defangv1.DeployResponse], error) { 20 | g.tries++ 21 | return nil, connect.NewError(connect.CodeUnavailable, errors.New("unavailable")) 22 | } 23 | 24 | // func (g *grpcMockHandler) Tail(ctx context.Context, r *connect.Request[defangv1.TailRequest], s *connect.ServerStream[defangv1.TailResponse]) error { 25 | // g.tries++ 26 | // return connect.NewError(connect.CodeUnavailable, errors.New("unavailable")) 27 | // } 28 | 29 | func TestRetrier(t *testing.T) { 30 | fabricServer := &grpcMockHandler{} 31 | _, handler := defangv1connect.NewFabricControllerHandler(fabricServer) 32 | 33 | server := httptest.NewTLSServer(handler) 34 | defer server.Close() 35 | 36 | fabricClient := defangv1connect.NewFabricControllerClient(server.Client(), server.URL, connect.WithGRPC(), connect.WithInterceptors(Retrier{})) 37 | 38 | _, err := fabricClient.Deploy(context.Background(), connect.NewRequest(&defangv1.DeployRequest{})) 39 | if err == nil { 40 | t.Fatal("expected error") 41 | } 42 | if fabricServer.tries != 2 { 43 | t.Fatalf("expected 2 tries, got %d", fabricServer.tries) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/pkg/cli/client/state.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | var ( 13 | stateDir, _ = userStateDir() 14 | // StateDir is the directory where the state file is stored 15 | StateDir = filepath.Join(stateDir, "defang") 16 | statePath = filepath.Join(StateDir, "state.json") 17 | state State 18 | ) 19 | 20 | type State struct { 21 | AnonID string 22 | TermsAcceptedAt time.Time 23 | } 24 | 25 | func initState(path string) State { 26 | state := State{AnonID: uuid.NewString()} 27 | if bytes, err := os.ReadFile(path); err == nil { 28 | json.Unmarshal(bytes, &state) 29 | } else { // could be not found or path error 30 | state.write(path) 31 | } 32 | return state 33 | } 34 | 35 | func (state State) write(path string) error { 36 | if bytes, err := json.MarshalIndent(state, "", " "); err != nil { 37 | return err 38 | } else { 39 | os.MkdirAll(StateDir, 0700) 40 | return os.WriteFile(path, bytes, 0600) 41 | } 42 | } 43 | 44 | func (state *State) acceptTerms() error { 45 | state.TermsAcceptedAt = time.Now() 46 | return state.write(statePath) 47 | } 48 | 49 | func (state State) termsAccepted() bool { 50 | // Consider the terms accepted if the timestamp is within the last 24 hours 51 | return time.Since(state.TermsAcceptedAt) < 24*time.Hour 52 | } 53 | 54 | func GetAnonID() string { 55 | state = initState(statePath) 56 | return state.AnonID 57 | } 58 | 59 | func AcceptTerms() error { 60 | return state.acceptTerms() 61 | } 62 | 63 | func TermsAccepted() bool { 64 | return state.termsAccepted() 65 | } 66 | -------------------------------------------------------------------------------- /src/pkg/cli/client/state_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package client 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | func userStateDir() (string, error) { 11 | if stateHome := os.Getenv("XDG_STATE_HOME"); stateHome != "" { 12 | return stateHome, nil 13 | } 14 | home, err := os.UserHomeDir() 15 | return filepath.Join(home, ".local", "state"), err 16 | } 17 | -------------------------------------------------------------------------------- /src/pkg/cli/client/state_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package client 4 | 5 | import ( 6 | "os" 7 | ) 8 | 9 | func userStateDir() (string, error) { 10 | return os.UserCacheDir() // %LocalAppData% 11 | } 12 | -------------------------------------------------------------------------------- /src/pkg/cli/client/waitForCdTaskExit.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "time" 8 | ) 9 | 10 | var pollDuration = 2 * time.Second 11 | 12 | func WaitForCdTaskExit(ctx context.Context, provider Provider) error { 13 | ticker := time.NewTicker(pollDuration) 14 | defer ticker.Stop() 15 | 16 | for { 17 | select { 18 | case <-ticker.C: 19 | if err := provider.GetDeploymentStatus(ctx); err != nil { 20 | if errors.Is(err, io.EOF) { 21 | // EOF indicates that the task has completed successfully 22 | return nil 23 | } 24 | return err 25 | } 26 | case <-ctx.Done(): // Stop the loop when the context is cancelled 27 | return ctx.Err() 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/pkg/cli/common.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "github.com/DefangLabs/defang/src/pkg/term" 8 | "google.golang.org/protobuf/encoding/protojson" 9 | "google.golang.org/protobuf/proto" 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | var ( 14 | DoDryRun = false 15 | 16 | ErrDryRun = errors.New("dry run") 17 | ) 18 | 19 | func MarshalPretty(root string, data proto.Message) ([]byte, error) { 20 | // HACK: convert to JSON first so we respect the json tags (like "omitempty") 21 | bytes, err := protojson.Marshal(data) 22 | if err != nil { 23 | return nil, err 24 | } 25 | var raw map[string]interface{} // TODO: this messes with the order of the fields 26 | if err := json.Unmarshal(bytes, &raw); err != nil { 27 | return nil, err 28 | } 29 | if root != "" { 30 | raw = map[string]interface{}{root: raw} 31 | } 32 | return yaml.Marshal(raw) 33 | } 34 | 35 | func PrintObject(root string, data proto.Message) error { 36 | bytes, err := MarshalPretty(root, data) 37 | if err != nil { 38 | return err 39 | } 40 | term.Println(string(bytes)) 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /src/pkg/cli/compose/GPUcounter.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | import ( 4 | "slices" 5 | 6 | composeTypes "github.com/compose-spec/compose-go/v2/types" 7 | ) 8 | 9 | func fixupDeviceCount(count composeTypes.DeviceCount) int { 10 | if count == -1 { 11 | return 1 12 | } 13 | return int(count) 14 | } 15 | 16 | func gpuDeviceCount(service *composeTypes.ServiceConfig) int { 17 | count := 0 18 | if service.Deploy != nil && 19 | service.Deploy.Resources.Reservations != nil { 20 | for _, device := range service.Deploy.Resources.Reservations.Devices { 21 | if slices.Contains(device.Capabilities, "gpu") { 22 | count += fixupDeviceCount(device.Count) 23 | } 24 | } 25 | } 26 | return count 27 | } 28 | 29 | func GetNumOfGPUs(services composeTypes.Services) int { 30 | numGPUs := 0 31 | for _, service := range services { 32 | numGPUs += gpuDeviceCount(&service) 33 | } 34 | return numGPUs 35 | } 36 | -------------------------------------------------------------------------------- /src/pkg/cli/compose/config_detector.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/DefangLabs/secret-detector/pkg/scanner" 7 | ) 8 | 9 | // assume that the input is a key-value pair string 10 | func detectConfig(input string) (detectorTypes []string, err error) { 11 | // Detectors check for certain formats in a string to determine if it contains a secret. 12 | // Some detectors allow additional configuration options, such as: 13 | // keyword: key contains a keyword (e.g. KEY, PASSWORD, SECRET, TOKEN, etc.) 14 | // high_entropy_string: calculated high entropy (randomness) in a string 15 | // These detectors require an entropy threshold value (0 = low entropy, 4+ = very high entropy). 16 | 17 | // create a custom scanner config 18 | cfg := scanner.NewConfigWithDefaults() 19 | cfg.Transformers = []string{"json"} 20 | cfg.DetectorConfigs["keyword"] = []string{"3"} 21 | cfg.DetectorConfigs["high_entropy_string"] = []string{"4"} 22 | 23 | // create a scanner from scanner config 24 | scannerClient, err := scanner.NewScannerFromConfig(cfg) 25 | if err != nil { 26 | return nil, fmt.Errorf("failed to make a config detector: %w", err) 27 | } 28 | 29 | // scan the input 30 | ds, err := scannerClient.Scan(input) 31 | if err != nil { 32 | return nil, fmt.Errorf("failed to scan input: %w", err) 33 | } 34 | 35 | // return a list of detector types 36 | list := []string{} 37 | for _, d := range ds { 38 | list = append(list, d.Type) 39 | } 40 | 41 | return list, nil 42 | } 43 | -------------------------------------------------------------------------------- /src/pkg/cli/compose/config_detector_test.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | import "testing" 4 | 5 | func TestDetectConfig(t *testing.T) { 6 | tests := []struct { 7 | input string 8 | expectedOutput []string 9 | }{ 10 | {"", nil}, 11 | {"not a secret", nil}, 12 | {"/leaderboard/api/hubs", nil}, 13 | {"https://user:p455w0rd@example.com", []string{"URL with password"}}, 14 | {"LINK: https://user:p455w0rd@example.com, LINK: https://user:p845w0rd@example.com", []string{"URL with password", "URL with password"}}, 15 | {"api-key=50m34p1k3y", []string{"Keyword Detector"}}, 16 | {"VEfk5vO0Q53VkK_uicor", []string{"High entropy string"}}, 17 | {"ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890", []string{"Github authentication"}}, 18 | {"AROA1234567890ABCDEF", []string{"AWS Client ID"}}, 19 | } 20 | 21 | for _, tt := range tests { 22 | t.Run(tt.input, func(t *testing.T) { 23 | ds, err := detectConfig(tt.input) 24 | 25 | //check for error 26 | if err != nil { 27 | if len(tt.expectedOutput) > 0 && tt.expectedOutput[0] != "" { 28 | t.Errorf("Error: %v", err) 29 | } 30 | return 31 | } 32 | 33 | // check for length of the output 34 | if len(ds) != len(tt.expectedOutput) { 35 | t.Errorf("Expected %d detector types, but got %d", len(tt.expectedOutput), len(ds)) 36 | return 37 | } 38 | 39 | // check for the output values 40 | for i, d := range ds { 41 | if d != tt.expectedOutput[i] { 42 | t.Errorf("Expected detector type %s, but got %s", tt.expectedOutput[i], d) 43 | } 44 | } 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/pkg/cli/compose/constants.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | const Mode_INGRESS = "ingress" 4 | const Mode_HOST = "host" 5 | 6 | const Protocol_TCP = "tcp" 7 | const Protocol_UDP = "udp" 8 | -------------------------------------------------------------------------------- /src/pkg/cli/compose/convert.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/DefangLabs/defang/src/pkg/term" 7 | composeTypes "github.com/compose-spec/compose-go/v2/types" 8 | ) 9 | 10 | func getResourceReservations(r composeTypes.Resources) *composeTypes.Resource { 11 | if r.Reservations == nil { 12 | // TODO: we might not want to default to all the limits, maybe only memory? 13 | return r.Limits 14 | } 15 | return r.Reservations 16 | } 17 | 18 | func fixupPort(port *composeTypes.ServicePortConfig) { 19 | switch port.Mode { 20 | case "": 21 | term.Warnf("port %d: no 'mode' was specified; defaulting to 'ingress' (add 'mode: ingress' to silence)", port.Target) 22 | fallthrough 23 | case Mode_INGRESS: 24 | // This code is unnecessarily complex because compose-go silently converts short `ports:` syntax to ingress+tcp 25 | if port.Protocol != Protocol_UDP { 26 | if port.Published != "" { 27 | term.Debugf("port %d: ignoring 'published: %s' in 'ingress' mode", port.Target, port.Published) 28 | } 29 | if (port.Protocol == Protocol_TCP || port.Protocol == Protocol_UDP) && port.AppProtocol != "http" { 30 | // TCP ingress is not supported; assuming HTTP (remove 'protocol' to silence)" 31 | port.AppProtocol = "http" 32 | } 33 | break 34 | } 35 | term.Warnf("port %d: UDP ports default to 'host' mode (add 'mode: host' to silence)", port.Target) 36 | port.Mode = Mode_HOST 37 | case Mode_HOST: 38 | // no-op 39 | default: 40 | panic(fmt.Sprintf("port %d: 'mode' should have been validated to be one of [host ingress] but got: %v", port.Target, port.Mode)) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/pkg/cli/compose/fixup_test.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/DefangLabs/defang/src/pkg/cli/client" 9 | composeTypes "github.com/compose-spec/compose-go/v2/types" 10 | ) 11 | 12 | func TestFixup(t *testing.T) { 13 | testRunCompose(t, func(t *testing.T, path string) { 14 | loader := NewLoader(WithPath(path)) 15 | proj, err := loader.LoadProject(context.Background()) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | err = FixupServices(context.Background(), client.MockProvider{}, proj, UploadModeIgnore) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | services := map[string]composeTypes.ServiceConfig{} 25 | for _, svc := range proj.Services { 26 | services[svc.Name] = svc 27 | } 28 | 29 | // Convert the protobuf services to pretty JSON for comparison (YAML would include all the zero values) 30 | actual, err := json.MarshalIndent(services, "", " ") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | if err := compare(actual, path+".fixup"); err != nil { 36 | t.Error(err) 37 | } 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /src/pkg/cli/compose/load_content.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/compose-spec/compose-go/v2/loader" 7 | composeTypes "github.com/compose-spec/compose-go/v2/types" 8 | ) 9 | 10 | func LoadFromContent(ctx context.Context, content []byte, nameFallback string) (*Project, error) { 11 | return loader.LoadWithContext(ctx, composeTypes.ConfigDetails{ConfigFiles: []composeTypes.ConfigFile{{Content: content}}}, func(o *loader.Options) { 12 | o.SetProjectName(nameFallback, false) 13 | o.SkipConsistencyCheck = true // this matches the WithConsistency(false) option from the loader 14 | o.SkipInterpolation = true 15 | o.SkipResolveEnvironment = true 16 | o.SkipInclude = true 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /src/pkg/cli/compose/normalize.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | nonAlphanumeric = regexp.MustCompile(`[^a-zA-Z0-9]+`) 10 | ) 11 | 12 | func NormalizeServiceName(s string) string { 13 | // TODO: replace with the code from compose-go 14 | return nonAlphanumeric.ReplaceAllLiteralString(strings.ToLower(s), "-") 15 | } 16 | -------------------------------------------------------------------------------- /src/pkg/cli/compose/normalize_test.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | import "testing" 4 | 5 | func TestNormalizeServiceName(t *testing.T) { 6 | testCases := []struct { 7 | name string 8 | expected string 9 | }{ 10 | {name: "normal", expected: "normal"}, 11 | {name: "camelCase", expected: "camelcase"}, 12 | {name: "PascalCase", expected: "pascalcase"}, 13 | {name: "hyphen-ok", expected: "hyphen-ok"}, 14 | {name: "snake_case", expected: "snake-case"}, 15 | {name: "$ymb0ls", expected: "-ymb0ls"}, 16 | {name: "consecutive--hyphens", expected: "consecutive-hyphens"}, 17 | {name: "hyphen-$ymbol", expected: "hyphen-ymbol"}, 18 | {name: "_blaha", expected: "-blaha"}, 19 | } 20 | for _, tC := range testCases { 21 | t.Run(tC.name, func(t *testing.T) { 22 | actual := NormalizeServiceName(tC.name) 23 | if actual != tC.expected { 24 | t.Errorf("NormalizeServiceName() failed: expected %v, got %v", tC.expected, actual) 25 | } 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/pkg/cli/compose/stateful.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | import "strings" 4 | 5 | var statefulImages = []string{ 6 | "cassandra", 7 | "couchdb", 8 | "elasticsearch", 9 | "etcd", 10 | "influxdb", 11 | "mariadb", 12 | "minio", // could be stateless 13 | "mongo", 14 | "mssql/server", 15 | "mysql", 16 | "nats", 17 | "neo4j", 18 | "oracle/database", 19 | "percona", 20 | "postgres", 21 | "rabbitmq", 22 | "redis", 23 | "rethinkdb", 24 | "scylla", 25 | "timescaledb", 26 | "vault", 27 | "zookeeper", 28 | } 29 | 30 | func isStatefulImage(image string) bool { 31 | repo := strings.ToLower(strings.SplitN(image, ":", 2)[0]) 32 | for _, statefulImage := range statefulImages { 33 | if strings.HasSuffix(repo, statefulImage) { 34 | return true 35 | } 36 | } 37 | return false 38 | } 39 | -------------------------------------------------------------------------------- /src/pkg/cli/compose/stateful_test.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | import "testing" 4 | 5 | func TestIsStatefulImage(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | image string 9 | expected bool 10 | }{ 11 | { 12 | name: "Stateful image", 13 | image: "redis", 14 | expected: true, 15 | }, 16 | { 17 | name: "Stateful image with repo", 18 | image: "library/redis", 19 | expected: true, 20 | }, 21 | { 22 | name: "Stateful image with tag", 23 | image: "redis:6.0", 24 | expected: true, 25 | }, 26 | { 27 | name: "Stateful image with registry", 28 | image: "docker.io/redis", 29 | expected: true, 30 | }, 31 | { 32 | name: "Stateless image", 33 | image: "alpine:latest", 34 | expected: false, 35 | }, 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | if got := isStatefulImage(tt.image); got != tt.expected { 40 | t.Errorf("isStatefulImage() = %v, want %v", got, tt.expected) 41 | } 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/pkg/cli/configDelete.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/DefangLabs/defang/src/pkg/cli/client" 7 | "github.com/DefangLabs/defang/src/pkg/term" 8 | defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" 9 | ) 10 | 11 | func ConfigDelete(ctx context.Context, projectName string, provider client.Provider, names ...string) error { 12 | term.Debugf("Deleting config %v in project %q", names, projectName) 13 | 14 | if DoDryRun { 15 | return ErrDryRun 16 | } 17 | 18 | return provider.DeleteConfig(ctx, &defangv1.Secrets{Names: names, Project: projectName}) 19 | } 20 | -------------------------------------------------------------------------------- /src/pkg/cli/configDelete_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/DefangLabs/defang/src/pkg/cli/client" 8 | defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" 9 | ) 10 | 11 | func TestConfigDelete(t *testing.T) { 12 | ctx := context.Background() 13 | provider := MockConfigDeleteProvider{} 14 | 15 | t.Run("expect no error", func(t *testing.T) { 16 | if err := ConfigDelete(ctx, "test", provider, "test_name"); err != nil { 17 | t.Fatalf("ConfigDelete() error = %v", err) 18 | } 19 | }) 20 | 21 | t.Run("expect error on DryRun", func(t *testing.T) { 22 | DoDryRun = true 23 | t.Cleanup(func() { DoDryRun = false }) 24 | 25 | if err := ConfigDelete(ctx, "test", provider, "test_name"); err != ErrDryRun { 26 | t.Fatalf("Expected ErrDryRun, got %v", err) 27 | } 28 | }) 29 | } 30 | 31 | type MockConfigDeleteProvider struct { 32 | client.Provider 33 | } 34 | 35 | func (MockConfigDeleteProvider) DeleteConfig(ctx context.Context, req *defangv1.Secrets) error { 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /src/pkg/cli/configList.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/DefangLabs/defang/src/pkg/cli/client" 7 | "github.com/DefangLabs/defang/src/pkg/term" 8 | defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" 9 | ) 10 | 11 | type PrintConfig struct { 12 | Name string 13 | } 14 | 15 | func ConfigList(ctx context.Context, projectName string, provider client.Provider) error { 16 | term.Debugf("Listing config in project %q", projectName) 17 | 18 | config, err := provider.ListConfig(ctx, &defangv1.ListConfigsRequest{Project: projectName}) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | numConfigs := len(config.Names) 24 | if numConfigs == 0 { 25 | _, err := term.Warn("No configs found") 26 | return err 27 | } 28 | 29 | configNames := make([]PrintConfig, numConfigs) 30 | for i, c := range config.Names { 31 | configNames[i] = PrintConfig{Name: c} 32 | } 33 | 34 | return term.Table(configNames, []string{"Name"}) 35 | } 36 | -------------------------------------------------------------------------------- /src/pkg/cli/configSet.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/DefangLabs/defang/src/pkg/cli/client" 7 | "github.com/DefangLabs/defang/src/pkg/term" 8 | defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" 9 | ) 10 | 11 | func ConfigSet(ctx context.Context, projectName string, provider client.Provider, name string, value string) error { 12 | term.Debugf("Setting config %q in project %q", name, projectName) 13 | 14 | if DoDryRun { 15 | return ErrDryRun 16 | } 17 | 18 | return provider.PutConfig(ctx, &defangv1.PutConfigRequest{Project: projectName, Name: name, Value: value}) 19 | } 20 | -------------------------------------------------------------------------------- /src/pkg/cli/configSet_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/DefangLabs/defang/src/pkg/cli/client" 9 | defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" 10 | ) 11 | 12 | func TestConfigSet(t *testing.T) { 13 | ctx := context.Background() 14 | provider := MustHaveProjectNamePutConfigProvider{} 15 | 16 | t.Run("expect no error", func(t *testing.T) { 17 | err := ConfigSet(ctx, "test", provider, "test_name", "test_value") 18 | if err != nil { 19 | t.Fatalf("ConfigSet() error = %v", err) 20 | } 21 | }) 22 | 23 | t.Run("expect error on DryRun", func(t *testing.T) { 24 | DoDryRun = true 25 | t.Cleanup(func() { DoDryRun = false }) 26 | err := ConfigSet(ctx, "test", provider, "test_name", "test_value") 27 | if err != ErrDryRun { 28 | t.Fatalf("Expected ErrDryRun, got %v", err) 29 | } 30 | }) 31 | } 32 | 33 | type MustHaveProjectNamePutConfigProvider struct { 34 | client.Provider 35 | } 36 | 37 | func (m MustHaveProjectNamePutConfigProvider) PutConfig(ctx context.Context, req *defangv1.PutConfigRequest) error { 38 | if req.Project == "" { 39 | return errors.New("project name is missing") 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /src/pkg/cli/delete.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/DefangLabs/defang/src/pkg/cli/client" 8 | "github.com/DefangLabs/defang/src/pkg/term" 9 | "github.com/DefangLabs/defang/src/pkg/types" 10 | defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" 11 | ) 12 | 13 | func Delete(ctx context.Context, projectName string, c client.FabricClient, provider client.Provider, names ...string) (types.ETag, error) { 14 | term.Debug("Deleting service", names) 15 | 16 | if DoDryRun { 17 | return "", ErrDryRun 18 | } 19 | 20 | delegateDomain, err := c.GetDelegateSubdomainZone(ctx, &defangv1.GetDelegateSubdomainZoneRequest{}) // TODO: pass projectName 21 | if err != nil { 22 | term.Debug("GetDelegateSubdomainZone failed:", err) 23 | return "", errors.New("failed to get delegate domain") 24 | } 25 | 26 | resp, err := provider.Delete(ctx, &defangv1.DeleteRequest{Project: projectName, Names: names, DelegateDomain: delegateDomain.Zone}) 27 | if err != nil { 28 | return "", err 29 | } 30 | return resp.Etag, nil 31 | } 32 | -------------------------------------------------------------------------------- /src/pkg/cli/getVersion.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/DefangLabs/defang/src/pkg/cli/client" 7 | ) 8 | 9 | func GetVersion(ctx context.Context, client client.FabricClient) (string, error) { 10 | versions, err := client.GetVersions(ctx) 11 | if err != nil { 12 | return "", err 13 | } 14 | return versions.Fabric, nil 15 | } 16 | -------------------------------------------------------------------------------- /src/pkg/cli/logout.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/DefangLabs/defang/src/pkg/cli/client" 7 | "github.com/DefangLabs/defang/src/pkg/term" 8 | "github.com/bufbuild/connect-go" 9 | ) 10 | 11 | func Logout(ctx context.Context, client client.FabricClient) error { 12 | term.Debug("Logging out") 13 | err := client.RevokeToken(ctx) 14 | // Ignore unauthenticated errors, since we're logging out anyway 15 | if connect.CodeOf(err) != connect.CodeUnauthenticated { 16 | return err 17 | } 18 | // TODO: remove the cached token file 19 | // tokenFile := getTokenFile(fabric) 20 | // if err := os.Remove(tokenFile); err != nil { 21 | // return err 22 | // } 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /src/pkg/cli/new_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "context" 7 | "errors" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | ourHttp "github.com/DefangLabs/defang/src/pkg/http" 13 | ) 14 | 15 | type mockRoundTripper struct{} 16 | 17 | func (d mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 18 | res := httptest.NewRecorder() 19 | if req.URL.String() != "https://github.com/DefangLabs/samples/archive/refs/heads/main.tar.gz" { 20 | res.Code = 404 21 | } else { 22 | gz := gzip.NewWriter(res.Body) 23 | tar.NewWriter(gz).Close() 24 | gz.Close() 25 | } 26 | return res.Result(), nil 27 | } 28 | 29 | func TestInitFromSamples(t *testing.T) { 30 | t.Run("mock", func(t *testing.T) { 31 | oldClient := ourHttp.DefaultClient 32 | t.Cleanup(func() { ourHttp.DefaultClient = oldClient }) 33 | ourHttp.DefaultClient = &http.Client{Transport: mockRoundTripper{}} 34 | 35 | err := InitFromSamples(context.Background(), t.TempDir(), []string{"nonexisting"}) 36 | if err == nil { 37 | t.Fatal("Expected test to fail") 38 | } 39 | if !errors.Is(err, ErrSampleNotFound) { 40 | t.Errorf("Expected error to be %v, got %v", ErrSampleNotFound, err) 41 | } 42 | }) 43 | 44 | t.Run("wan", func(t *testing.T) { 45 | if testing.Short() { 46 | t.Skip("skipped; add -short to enable") 47 | } 48 | 49 | err := InitFromSamples(context.Background(), t.TempDir(), []string{"nonexisting"}) 50 | if err == nil { 51 | t.Fatal("Expected test to fail") 52 | } 53 | if !errors.Is(err, ErrSampleNotFound) { 54 | t.Errorf("Expected error to be %v, got %v", ErrSampleNotFound, err) 55 | } 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/pkg/cli/preview.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | 6 | cliClient "github.com/DefangLabs/defang/src/pkg/cli/client" 7 | "github.com/DefangLabs/defang/src/pkg/cli/compose" 8 | "github.com/DefangLabs/defang/src/pkg/logs" 9 | defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" 10 | ) 11 | 12 | func Preview(ctx context.Context, project *compose.Project, fabric cliClient.FabricClient, provider cliClient.Provider, mode defangv1.DeploymentMode) error { 13 | resp, project, err := ComposeUp(ctx, project, fabric, provider, compose.UploadModePreview, mode) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | options := TailOptions{Deployment: resp.Etag, LogType: logs.LogTypeBuild, Verbose: true} 19 | return TailAndWaitForCD(ctx, project.Name, provider, options, LogEntryPrintHandler) 20 | } 21 | -------------------------------------------------------------------------------- /src/pkg/cli/sendMsg.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/DefangLabs/defang/src/pkg/cli/client" 8 | "github.com/DefangLabs/defang/src/pkg/term" 9 | defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" 10 | "github.com/google/uuid" 11 | ) 12 | 13 | func SendMsg(ctx context.Context, client client.FabricClient, subject, _type, id string, data []byte, contenttype string) error { 14 | if subject == "" { 15 | return errors.New("subject is required") 16 | } 17 | if _type == "" { 18 | return errors.New("type is required") 19 | } 20 | if id == "" { 21 | id = uuid.NewString() 22 | } 23 | 24 | term.Debug("Sending message to", subject, "with type", _type, "and id", id) 25 | 26 | if DoDryRun { 27 | return ErrDryRun 28 | } 29 | 30 | err := client.Publish(ctx, &defangv1.PublishRequest{Event: &defangv1.Event{ 31 | Specversion: "1.0", 32 | Type: _type, 33 | Source: "https://cli.defang.io", 34 | Subject: subject, 35 | Id: id, 36 | Datacontenttype: contenttype, 37 | Data: data, 38 | }}) 39 | return err 40 | } 41 | -------------------------------------------------------------------------------- /src/pkg/cli/teardown.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/DefangLabs/defang/src/pkg/cli/client" 9 | "github.com/DefangLabs/defang/src/pkg/term" 10 | ) 11 | 12 | func TearDown(ctx context.Context, provider client.Provider, force bool) error { 13 | if DoDryRun { 14 | return errors.New("dry run") 15 | } 16 | if !force { 17 | if list, err := provider.BootstrapList(ctx); err != nil { 18 | return fmt.Errorf("could not get list of services; use --force to tear down anyway: %w", err) 19 | } else if len(list) > 0 { 20 | return errors.New("there are still deployed services; use --force to tear down anyway") 21 | } 22 | } 23 | term.Warn(`Deleting the CD cluster; this does not delete the services!`) 24 | return provider.TearDown(ctx) 25 | } 26 | -------------------------------------------------------------------------------- /src/pkg/cli/token.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/DefangLabs/defang/src/pkg/auth" 8 | "github.com/DefangLabs/defang/src/pkg/cli/client" 9 | "github.com/DefangLabs/defang/src/pkg/scope" 10 | "github.com/DefangLabs/defang/src/pkg/term" 11 | "github.com/DefangLabs/defang/src/pkg/types" 12 | defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" 13 | ) 14 | 15 | func Token(ctx context.Context, client client.FabricClient, tenant types.TenantName, dur time.Duration, s scope.Scope) error { 16 | if DoDryRun { 17 | return ErrDryRun 18 | } 19 | 20 | code, err := auth.StartAuthCodeFlow(ctx, true) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | at, err := auth.ExchangeCodeForToken(ctx, code, tenant, dur, s) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | // Translate the OpenAuth token to our own Defang Fabric token 31 | var scopes []string 32 | if s != scope.Admin { 33 | scopes = []string{string(s)} 34 | } 35 | 36 | resp, err := client.Token(ctx, &defangv1.TokenRequest{ 37 | Assertion: at, 38 | ExpiresIn: uint32(dur.Seconds()), 39 | Scope: scopes, 40 | Tenant: string(tenant), 41 | }) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | term.Printc(term.BrightCyan, "Scoped access token: ") 47 | term.Println(resp.AccessToken) 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/cfn/outputs/ids.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | const ( 4 | BucketName = "bucketName" 5 | ClusterName = "clusterName" 6 | LogGroupARN = "logGroupArn" 7 | SecurityGroupID = "securityGroupId" 8 | SubnetID = "subnetId" 9 | TaskDefArn = "taskDefArn" 10 | TemplateVersion = "templateVersion" 11 | ) 12 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/cfn/template_test.go: -------------------------------------------------------------------------------- 1 | package cfn 2 | 3 | import "testing" 4 | 5 | func TestGetCacheRepoPrefix(t *testing.T) { 6 | // Test cases 7 | tests := []struct { 8 | prefix string 9 | suffix string 10 | want string 11 | }{ 12 | { 13 | prefix: "short-", 14 | suffix: "ecr-public", 15 | want: "short-ecr-public", 16 | }, 17 | { 18 | prefix: "short-", 19 | suffix: "docker-public", 20 | want: "short-docker-public", 21 | }, 22 | { 23 | prefix: "loooooooooong-", 24 | suffix: "docker-public", 25 | want: "fab852-docker-public", 26 | }, 27 | } 28 | for _, tt := range tests { 29 | t.Run(tt.want, func(t *testing.T) { 30 | if got := getCacheRepoPrefix(tt.prefix, tt.suffix); got != tt.want { 31 | t.Errorf("getCacheRepoPrefix() = %q, want %q", got, tt.want) 32 | } else if len(got) > maxCachePrefixLength { 33 | t.Errorf("getCacheRepoPrefix() = %q, want length <= %v", got, maxCachePrefixLength) 34 | } 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/cfn/waiter.go: -------------------------------------------------------------------------------- 1 | package cfn 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/aws/aws-sdk-go-v2/service/cloudformation" 7 | ) 8 | 9 | const minDelay = 1 * time.Second 10 | 11 | // update1s is a functional option for cloudformation.StackUpdateCompleteWaiter that sets the MinDelay to 1s 12 | func update1s(o *cloudformation.StackUpdateCompleteWaiterOptions) { 13 | o.MinDelay = minDelay 14 | } 15 | 16 | // delete1s is a functional option for cloudformation.StackDeleteCompleteWaiter that sets the MinDelay to 1s 17 | func delete1s(o *cloudformation.StackDeleteCompleteWaiterOptions) { 18 | o.MinDelay = minDelay 19 | } 20 | 21 | // create1s is a functional option for cloudformation.StackCreateCompleteWaiter that sets the MinDelay to 1s 22 | func create1s(o *cloudformation.StackCreateCompleteWaiterOptions) { 23 | o.MinDelay = minDelay 24 | } 25 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/common_test.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPlatformToArch(t *testing.T) { 8 | tests := []struct { 9 | platform string 10 | wantArch string 11 | wantOs string 12 | }{ 13 | {"", "", ""}, 14 | {"blah", "BLAH", ""}, // invalid platform 15 | {"amd64", "X86_64", ""}, 16 | {"arm64", "ARM64", ""}, 17 | {"linux/amd64", "X86_64", "LINUX"}, 18 | {"linux/arm64", "ARM64", "LINUX"}, 19 | {"linux/arm64/v8", "ARM64", "LINUX"}, 20 | {"linux/blah", "BLAH", "LINUX"}, // invalid platform 21 | {"windows/blah", "BLAH", "WINDOWS"}, // invalid platform 22 | {"windows/amd64", "X86_64", "WINDOWS"}, 23 | } 24 | for _, tt := range tests { 25 | t.Run(tt.platform, func(t *testing.T) { 26 | arch, os := PlatformToArchOS(tt.platform) 27 | if os != tt.wantOs { 28 | t.Errorf("PlatformToArch() os = %q, want %q", os, tt.wantOs) 29 | } 30 | if arch != tt.wantArch { 31 | t.Errorf("PlatformToArch() arch = %q, want %q", arch, tt.wantArch) 32 | } 33 | }) 34 | } 35 | } 36 | 37 | func TestGetAccountID(t *testing.T) { 38 | a := AwsEcs{ 39 | TaskDefARN: "arn:aws:ecs:us-east-1:123456789012:task-definition/defang-ecs-2021-08-31-163042", 40 | } 41 | if got := a.getAccountID(); got != "123456789012" { 42 | t.Errorf("GetAccountID() = %v, want 123456789012", got) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/ecsserviceaction/aws_event.go: -------------------------------------------------------------------------------- 1 | package ecsserviceaction 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type AWSEvent struct { 8 | Detail ECSServiceAction `json:"detail"` 9 | Account string `json:"account"` 10 | DetailType string `json:"detail-type"` 11 | Id string `json:"id"` 12 | Region string `json:"region"` 13 | Resources []string `json:"resources"` 14 | Source string `json:"source"` 15 | Time time.Time `json:"time"` 16 | Version string `json:"version"` 17 | } 18 | 19 | func (a *AWSEvent) SetDetail(detail ECSServiceAction) { 20 | a.Detail = detail 21 | } 22 | 23 | func (a *AWSEvent) SetAccount(account string) { 24 | a.Account = account 25 | } 26 | 27 | func (a *AWSEvent) SetDetailType(detailType string) { 28 | a.DetailType = detailType 29 | } 30 | 31 | func (a *AWSEvent) SetId(id string) { 32 | a.Id = id 33 | } 34 | 35 | func (a *AWSEvent) SetRegion(region string) { 36 | a.Region = region 37 | } 38 | 39 | func (a *AWSEvent) SetResources(resources []string) { 40 | a.Resources = resources 41 | } 42 | 43 | func (a *AWSEvent) SetSource(source string) { 44 | a.Source = source 45 | } 46 | 47 | func (a *AWSEvent) SetTime(time time.Time) { 48 | a.Time = time 49 | } 50 | 51 | func (a *AWSEvent) SetVersion(version string) { 52 | a.Version = version 53 | } 54 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/ecsserviceaction/marshaller.go: -------------------------------------------------------------------------------- 1 | package ecsserviceaction 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | func Marshal(inputEvent interface{}) ([]byte, error) { 8 | outputStream, err := json.Marshal(inputEvent) 9 | if err != nil { 10 | return nil, err 11 | } 12 | 13 | return outputStream, nil 14 | } 15 | 16 | func Unmarshal(inputStream []byte) (map[string]interface{}, error) { 17 | var outputEvent map[string]interface{} 18 | err := json.Unmarshal(inputStream, &outputEvent) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | return outputEvent, nil 24 | } 25 | 26 | func UnmarshalEvent(inputStream []byte) (AWSEvent, error) { 27 | var outputEvent AWSEvent 28 | err := json.Unmarshal(inputStream, &outputEvent) 29 | if err != nil { 30 | return AWSEvent{}, err 31 | } 32 | 33 | return outputEvent, nil 34 | } 35 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/ecstaskstatechange/attachment_details.go: -------------------------------------------------------------------------------- 1 | package ecstaskstatechange 2 | 3 | type AttachmentDetails struct { 4 | Id string `json:"id,omitempty"` 5 | AttachmentDetailsType string `json:"type,omitempty"` 6 | Status string `json:"status,omitempty"` 7 | Details []AttachmentDetails_details `json:"details,omitempty"` 8 | } 9 | 10 | func (a *AttachmentDetails) SetId(id string) { 11 | a.Id = id 12 | } 13 | 14 | func (a *AttachmentDetails) SetAttachmentDetailsType(attachmentDetailsType string) { 15 | a.AttachmentDetailsType = attachmentDetailsType 16 | } 17 | 18 | func (a *AttachmentDetails) SetStatus(status string) { 19 | a.Status = status 20 | } 21 | 22 | func (a *AttachmentDetails) SetDetails(details []AttachmentDetails_details) { 23 | a.Details = details 24 | } 25 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/ecstaskstatechange/attachment_details_details.go: -------------------------------------------------------------------------------- 1 | package ecstaskstatechange 2 | 3 | type AttachmentDetails_details struct { 4 | Name string `json:"name,omitempty"` 5 | Value string `json:"value,omitempty"` 6 | } 7 | 8 | func (a *AttachmentDetails_details) SetName(name string) { 9 | a.Name = name 10 | } 11 | 12 | func (a *AttachmentDetails_details) SetValue(value string) { 13 | a.Value = value 14 | } 15 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/ecstaskstatechange/attributes_details.go: -------------------------------------------------------------------------------- 1 | package ecstaskstatechange 2 | 3 | type AttributesDetails struct { 4 | Name string `json:"name,omitempty"` 5 | Value string `json:"value,omitempty"` 6 | } 7 | 8 | func (a *AttributesDetails) SetName(name string) { 9 | a.Name = name 10 | } 11 | 12 | func (a *AttributesDetails) SetValue(value string) { 13 | a.Value = value 14 | } 15 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/ecstaskstatechange/aws_event.go: -------------------------------------------------------------------------------- 1 | package ecstaskstatechange 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type AWSEvent struct { 8 | Detail ECSTaskStateChange `json:"detail"` 9 | DetailType string `json:"detail-type"` 10 | Resources []string `json:"resources"` 11 | Id string `json:"id"` 12 | Source string `json:"source"` 13 | Time time.Time `json:"time"` 14 | Region string `json:"region"` 15 | Version string `json:"version"` 16 | Account string `json:"account"` 17 | } 18 | 19 | func (a *AWSEvent) SetDetail(detail ECSTaskStateChange) { 20 | a.Detail = detail 21 | } 22 | 23 | func (a *AWSEvent) SetDetailType(detailType string) { 24 | a.DetailType = detailType 25 | } 26 | 27 | func (a *AWSEvent) SetResources(resources []string) { 28 | a.Resources = resources 29 | } 30 | 31 | func (a *AWSEvent) SetId(id string) { 32 | a.Id = id 33 | } 34 | 35 | func (a *AWSEvent) SetSource(source string) { 36 | a.Source = source 37 | } 38 | 39 | func (a *AWSEvent) SetTime(time time.Time) { 40 | a.Time = time 41 | } 42 | 43 | func (a *AWSEvent) SetRegion(region string) { 44 | a.Region = region 45 | } 46 | 47 | func (a *AWSEvent) SetVersion(version string) { 48 | a.Version = version 49 | } 50 | 51 | func (a *AWSEvent) SetAccount(account string) { 52 | a.Account = account 53 | } 54 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/ecstaskstatechange/environment.go: -------------------------------------------------------------------------------- 1 | package ecstaskstatechange 2 | 3 | type Environment struct { 4 | Name string `json:"name"` 5 | Value string `json:"value"` 6 | } 7 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/ecstaskstatechange/marshaller.go: -------------------------------------------------------------------------------- 1 | package ecstaskstatechange 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | func Marshal(inputEvent interface{}) ([]byte, error) { 8 | outputStream, err := json.Marshal(inputEvent) 9 | if err != nil { 10 | return nil, err 11 | } 12 | 13 | return outputStream, nil 14 | } 15 | 16 | func Unmarshal(inputStream []byte) (map[string]interface{}, error) { 17 | var outputEvent map[string]interface{} 18 | err := json.Unmarshal(inputStream, &outputEvent) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | return outputEvent, nil 24 | } 25 | 26 | func UnmarshalEvent(inputStream []byte) (AWSEvent, error) { 27 | var outputEvent AWSEvent 28 | err := json.Unmarshal(inputStream, &outputEvent) 29 | if err != nil { 30 | return AWSEvent{}, err 31 | } 32 | 33 | return outputEvent, nil 34 | } 35 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/ecstaskstatechange/network_binding_details.go: -------------------------------------------------------------------------------- 1 | package ecstaskstatechange 2 | 3 | type NetworkBindingDetails struct { 4 | BindIP string `json:"bindIP,omitempty"` 5 | Protocol string `json:"protocol,omitempty"` 6 | ContainerPort float64 `json:"containerPort,omitempty"` 7 | HostPort float64 `json:"hostPort,omitempty"` 8 | } 9 | 10 | func (n *NetworkBindingDetails) SetBindIP(bindIP string) { 11 | n.BindIP = bindIP 12 | } 13 | 14 | func (n *NetworkBindingDetails) SetProtocol(protocol string) { 15 | n.Protocol = protocol 16 | } 17 | 18 | func (n *NetworkBindingDetails) SetContainerPort(containerPort float64) { 19 | n.ContainerPort = containerPort 20 | } 21 | 22 | func (n *NetworkBindingDetails) SetHostPort(hostPort float64) { 23 | n.HostPort = hostPort 24 | } 25 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/ecstaskstatechange/network_interface_details.go: -------------------------------------------------------------------------------- 1 | package ecstaskstatechange 2 | 3 | type NetworkInterfaceDetails struct { 4 | PrivateIpv4Address string `json:"privateIpv4Address,omitempty"` 5 | Ipv6Address string `json:"ipv6Address,omitempty"` 6 | AttachmentId string `json:"attachmentId,omitempty"` 7 | } 8 | 9 | func (n *NetworkInterfaceDetails) SetPrivateIpv4Address(privateIpv4Address string) { 10 | n.PrivateIpv4Address = privateIpv4Address 11 | } 12 | 13 | func (n *NetworkInterfaceDetails) SetIpv6Address(ipv6Address string) { 14 | n.Ipv6Address = ipv6Address 15 | } 16 | 17 | func (n *NetworkInterfaceDetails) SetAttachmentId(attachmentId string) { 18 | n.AttachmentId = attachmentId 19 | } 20 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/ecstaskstatechange/overrides.go: -------------------------------------------------------------------------------- 1 | package ecstaskstatechange 2 | 3 | type Overrides struct { 4 | ContainerOverrides []OverridesItem `json:"containerOverrides"` 5 | } 6 | 7 | func (o *Overrides) SetContainerOverrides(containerOverrides []OverridesItem) { 8 | o.ContainerOverrides = containerOverrides 9 | } 10 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/ecstaskstatechange/overrides_item.go: -------------------------------------------------------------------------------- 1 | package ecstaskstatechange 2 | 3 | type OverridesItem struct { 4 | Environment []Environment `json:"environment,omitempty"` 5 | Memory float64 `json:"memory,omitempty"` 6 | Name string `json:"name"` 7 | Cpu float64 `json:"cpu,omitempty"` 8 | Command []string `json:"command,omitempty"` 9 | } 10 | 11 | func (o *OverridesItem) SetEnvironment(environment []Environment) { 12 | o.Environment = environment 13 | } 14 | 15 | func (o *OverridesItem) SetMemory(memory float64) { 16 | o.Memory = memory 17 | } 18 | 19 | func (o *OverridesItem) SetName(name string) { 20 | o.Name = name 21 | } 22 | 23 | func (o *OverridesItem) SetCpu(cpu float64) { 24 | o.Cpu = cpu 25 | } 26 | 27 | func (o *OverridesItem) SetCommand(command []string) { 28 | o.Command = command 29 | } 30 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/fargate.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | type CpuUnits = uint 9 | type MemoryMiB = uint 10 | 11 | func makeMinMaxCeil(value float64, minValue, maxValue, step uint) uint { 12 | if value <= float64(minValue) || math.IsNaN(value) { 13 | return minValue 14 | } else if value >= float64(maxValue) { 15 | return maxValue 16 | } 17 | return uint(math.Ceil(value/float64(step))) * step 18 | } 19 | 20 | func fixupFargateCPU(vCpu float64) CpuUnits { 21 | return 1 << makeMinMaxCeil(math.Log2(vCpu)+10, 8, 14, 1) // 256…16384 22 | } 23 | 24 | func fixupFargateMemory(cpu CpuUnits, memoryMiB float64) MemoryMiB { 25 | switch cpu { 26 | case 256: // 0.25 vCPU 27 | return makeMinMaxCeil(memoryMiB, 512, 2048, 1024) 28 | case 512: // 0.5 vCPU 29 | return makeMinMaxCeil(memoryMiB, 1024, 4096, 1024) 30 | case 1024: // 1 vCPU 31 | return makeMinMaxCeil(memoryMiB, 2048, 8192, 1024) 32 | case 2048: // 2 vCPU 33 | return makeMinMaxCeil(memoryMiB, 4096, 16384, 1024) 34 | case 4096: // 4 vCPU 35 | return makeMinMaxCeil(memoryMiB, 8192, 30720, 1024) 36 | case 8192: // 8 vCPU 37 | return makeMinMaxCeil(memoryMiB, 16384, 61440, 4096) 38 | case 16384: // 16 vCPU 39 | return makeMinMaxCeil(memoryMiB, 32768, 122880, 4096) 40 | default: 41 | panic(fmt.Sprintf("Unsupported value for cpu: %v", cpu)) 42 | } 43 | } 44 | 45 | func FixupFargateConfig(vCpu, memoryMiB float64) (cpu CpuUnits, memory MemoryMiB) { 46 | for cpu = fixupFargateCPU(vCpu); ; cpu *= 2 { 47 | memory = fixupFargateMemory(cpu, memoryMiB) 48 | if float64(memory) >= memoryMiB { 49 | return 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/logs_test.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestLogGroupIdentifier(t *testing.T) { 10 | arn := "arn:aws:logs:us-west-2:123456789012:log-group:/LOG/GROUP/NAME:*" 11 | expected := "arn:aws:logs:us-west-2:123456789012:log-group:/LOG/GROUP/NAME" 12 | if got := getLogGroupIdentifier(arn); got != expected { 13 | t.Errorf("Expected %q, but got %q", expected, got) 14 | } 15 | if got := getLogGroupIdentifier(expected); got != expected { 16 | t.Errorf("Expected %q, but got %q", expected, got) 17 | } 18 | } 19 | 20 | func TestSplitClusterTask(t *testing.T) { 21 | taskArn := "arn:aws:ecs:us-west-2:123456789012:task/cluster-name/12345678123412341234123456789012" 22 | expectedClusterName := "cluster-name" 23 | 24 | clusterName, taskID := SplitClusterTask(&taskArn) 25 | 26 | if clusterName != expectedClusterName { 27 | t.Errorf("Expected cluster name %q, but got %q", expectedClusterName, clusterName) 28 | } 29 | if taskID != "12345678123412341234123456789012" { 30 | t.Errorf("Expected task ID %q, but got %q", taskArn, taskID) 31 | } 32 | } 33 | 34 | func TestQueryAndTailLogGroups(t *testing.T) { 35 | e, err := QueryAndTailLogGroups(context.Background(), time.Now(), time.Time{}) 36 | if err != nil { 37 | t.Errorf("Expected no error, but got: %v", err) 38 | } 39 | if e.Err() != nil { 40 | t.Errorf("Expected no error, but got: %v", e.Err()) 41 | } 42 | err = e.Close() 43 | if err != nil { 44 | t.Errorf("Expected no error, but got: %v", err) 45 | } 46 | _, ok := <-e.Events() 47 | if ok { 48 | t.Error("Expected channel to be closed") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/merge.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | // Inspired by https://dev.to/vinaygo/concurrency-merge-sort-using-channels-and-goroutines-in-golang-35f7 4 | func Mergech[T any](left chan T, right chan T, c chan T, less func(T, T) bool) { 5 | defer close(c) 6 | val, ok := <-left 7 | val2, ok2 := <-right 8 | for ok && ok2 { 9 | if less(val, val2) { 10 | c <- val 11 | val, ok = <-left 12 | } else { 13 | c <- val2 14 | val2, ok2 = <-right 15 | } 16 | } 17 | for ok { 18 | c <- val 19 | val, ok = <-left 20 | } 21 | for ok2 { 22 | c <- val2 23 | val2, ok2 = <-right 24 | } 25 | } 26 | 27 | func mergeLogEventChan(left, right chan LogEvent) chan LogEvent { 28 | if left == nil { 29 | return right 30 | } 31 | if right == nil { 32 | return left 33 | } 34 | out := make(chan LogEvent) 35 | go Mergech(left, right, out, func(i1, i2 LogEvent) bool { 36 | return *i1.Timestamp < *i2.Timestamp 37 | }) 38 | return out 39 | } 40 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/stop.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/DefangLabs/defang/src/pkg/types" 7 | "github.com/aws/aws-sdk-go-v2/service/ecs" 8 | "github.com/aws/smithy-go/ptr" 9 | ) 10 | 11 | func (a AwsEcs) Stop(ctx context.Context, id types.TaskID) error { 12 | cfg, err := a.LoadConfig(ctx) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | _, err = ecs.NewFromConfig(cfg).StopTask(ctx, &ecs.StopTaskInput{ 18 | Cluster: ptr.String(a.ClusterName), 19 | Task: id, 20 | // Reason: ptr.String("defang stop"), 21 | }) 22 | return err 23 | } 24 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/stream_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package ecs 4 | 5 | import ( 6 | "context" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestPendingStream(t *testing.T) { 12 | if testing.Short() { 13 | t.Skip("skipping slow integration test") 14 | } 15 | 16 | ps, _ := QueryAndTailLogGroup(context.Background(), LogGroupInput{ 17 | LogGroupARN: "arn:aws:logs:us-west-2:532501343364:log-group:/ecs/lio/logss:*", 18 | }, time.Now().Add(-time.Minute), time.Time{}) 19 | 20 | go func() { 21 | time.Sleep(5 * time.Second) 22 | ps.Close() 23 | }() 24 | 25 | if ps.Err() != nil { 26 | t.Errorf("Error: %v", ps.Err()) 27 | } 28 | 29 | for e := range ps.Events() { 30 | if e == nil { 31 | t.Errorf("Error: %v", ps.Err()) 32 | } 33 | println(e) 34 | } 35 | t.Error(ps.Err()) 36 | } 37 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/tail_test.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetLogStreamForTaskID(t *testing.T) { 8 | expectedLogStream := "prefix/main_app/12345678123412341234123456789012" 9 | 10 | logStream := GetLogStreamForTaskID("prefix", "main_app", "12345678123412341234123456789012") 11 | 12 | if logStream != expectedLogStream { 13 | t.Errorf("Expected log stream %q, but got %q", expectedLogStream, logStream) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/ecs/upload.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "regexp" 7 | 8 | "github.com/aws/aws-sdk-go-v2/service/s3" 9 | "github.com/aws/smithy-go/ptr" 10 | "github.com/google/uuid" 11 | ) 12 | 13 | // From https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html 14 | var s3InvalidCharsRegexp = regexp.MustCompile(`[^a-zA-Z0-9!_.*'()-]`) 15 | 16 | const prefix = "uploads/" 17 | 18 | func (a *AwsEcs) CreateUploadURL(ctx context.Context, name string) (string, error) { 19 | cfg, err := a.LoadConfig(ctx) 20 | if err != nil { 21 | return "", err 22 | } 23 | 24 | if name == "" { 25 | name = uuid.NewString() 26 | } else { 27 | if len(name) > 64 { 28 | return "", errors.New("name must be less than 64 characters") 29 | } 30 | // Sanitize the digest so it's safe to use as a file name 31 | name = s3InvalidCharsRegexp.ReplaceAllString(name, "_") 32 | // name = path.Join(buildsPath, tenantName.String(), digest); TODO: avoid collisions between tenants 33 | } 34 | 35 | s3Client := s3.NewFromConfig(cfg) 36 | // Use S3 SDK to create a presigned URL for uploading a file. 37 | req, err := s3.NewPresignClient(s3Client).PresignPutObject(ctx, &s3.PutObjectInput{ 38 | Bucket: &a.BucketName, 39 | Key: ptr.String(prefix + name), 40 | }) 41 | if err != nil { 42 | return "", err 43 | } 44 | return req.URL, nil 45 | } 46 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/region/region.go: -------------------------------------------------------------------------------- 1 | package region 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/DefangLabs/defang/src/pkg/clouds/aws" 7 | ) 8 | 9 | type Region = aws.Region 10 | 11 | const ( 12 | AFSouth1 Region = "af-south-1" 13 | APEast1 Region = "ap-east-1" 14 | APNortheast1 Region = "ap-northeast-1" 15 | APNortheast2 Region = "ap-northeast-2" 16 | APNortheast3 Region = "ap-northeast-3" 17 | APSouth1 Region = "ap-south-1" 18 | APSouth2 Region = "ap-south-2" 19 | APSoutheast1 Region = "ap-southeast-1" 20 | APSoutheast2 Region = "ap-southeast-2" 21 | APSoutheast3 Region = "ap-southeast-3" 22 | APSoutheast4 Region = "ap-southeast-4" 23 | CACentral Region = "ca-central-1" 24 | CNNorth1 Region = "cn-north-1" 25 | CNNorthwest1 Region = "cn-northwest-1" 26 | EUCentral1 Region = "eu-central-1" 27 | EUCentral2 Region = "eu-central-2" 28 | EUNorth1 Region = "eu-north-1" 29 | EUSouth1 Region = "eu-south-1" 30 | EUSouth2 Region = "eu-south-2" 31 | EUWest1 Region = "eu-west-1" 32 | EUWest2 Region = "eu-west-2" 33 | EUWest3 Region = "eu-west-3" 34 | MECentral1 Region = "me-central-1" 35 | MESouth1 Region = "me-south-1" 36 | SAEast1 Region = "sa-east-1" 37 | USGovEast1 Region = "us-gov-east-1" 38 | USGovWest1 Region = "us-gov-west-1" 39 | USEast1 Region = "us-east-1" 40 | USEast2 Region = "us-east-2" 41 | USWest1 Region = "us-west-1" 42 | USWest2 Region = "us-west-2" 43 | ) 44 | 45 | func FromArn(arn string) Region { 46 | parts := strings.Split(arn, ":") 47 | if len(parts) < 6 || parts[0] != "arn" { 48 | panic("invalid ARN") 49 | } 50 | return Region(parts[3]) 51 | } 52 | -------------------------------------------------------------------------------- /src/pkg/clouds/aws/s3.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/aws/aws-sdk-go-v2/service/s3/types" 7 | ) 8 | 9 | type ErrNoSuchKey = types.NoSuchKey 10 | 11 | // Deprecated: use ErrNoSuchKey directly 12 | func IsS3NoSuchKeyError(err error) bool { 13 | var e *types.NoSuchKey 14 | return errors.As(err, &e) 15 | } 16 | -------------------------------------------------------------------------------- /src/pkg/clouds/do/appPlatform/parse.go: -------------------------------------------------------------------------------- 1 | package appPlatform 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | // Copied from defang-mvp/pulumi/shared/utils.ts 9 | var IMG_RE = regexp.MustCompile(`^(?:(.{1,127}?)\/)?(.{1,127}?)(?::(\w[\w.-]{0,127}))?(?:@(sha256:[0-9a-f]{64}))?$`) 10 | 11 | type Image struct { 12 | Registry string 13 | Repo string 14 | Tag string 15 | Digest string 16 | } 17 | 18 | func ParseImage(image string) (*Image, error) { 19 | parts := IMG_RE.FindStringSubmatch(image) 20 | if parts == nil { 21 | return nil, fmt.Errorf("invalid image: %s", image) 22 | } 23 | return &Image{ 24 | Registry: parts[1], 25 | Repo: parts[2], 26 | Tag: parts[3], 27 | Digest: parts[4], 28 | }, nil 29 | } 30 | -------------------------------------------------------------------------------- /src/pkg/clouds/do/appPlatform/parse_test.go: -------------------------------------------------------------------------------- 1 | package appPlatform 2 | 3 | import "testing" 4 | 5 | func TestParseImage(t *testing.T) { 6 | tests := []struct { 7 | image string 8 | want Image 9 | }{ 10 | { 11 | image: "docker.io/pulumi/pulumi:latest", 12 | want: Image{ 13 | Registry: "docker.io", 14 | Repo: "pulumi/pulumi", 15 | Tag: "latest", 16 | }, 17 | }, 18 | { 19 | image: "redis", 20 | want: Image{ 21 | Repo: "redis", 22 | }, 23 | }, 24 | { 25 | image: "defangio/cd@sha256:2e671c45664af2a40cc9e78dfbf3c985c7f89746b8a62712273c158f3436266a", 26 | want: Image{ 27 | Registry: "defangio", 28 | Repo: "cd", 29 | Digest: "sha256:2e671c45664af2a40cc9e78dfbf3c985c7f89746b8a62712273c158f3436266a", 30 | }, 31 | }, 32 | { 33 | image: "docker.io/pulumi/pulumi:latest@sha256:2e671c45664af2a40cc9e78dfbf3c985c7f89746b8a62712273c158f3436266a", 34 | want: Image{ 35 | Registry: "docker.io", 36 | Repo: "pulumi/pulumi", 37 | Tag: "latest", 38 | Digest: "sha256:2e671c45664af2a40cc9e78dfbf3c985c7f89746b8a62712273c158f3436266a", 39 | }, 40 | }, 41 | } 42 | 43 | for _, tt := range tests { 44 | t.Run(tt.image, func(t *testing.T) { 45 | got, err := ParseImage(tt.image) 46 | if err != nil { 47 | t.Fatalf("ParseImage(%s) got error: %v", tt.image, err) 48 | } 49 | if got.Registry != tt.want.Registry || got.Repo != tt.want.Repo || got.Tag != tt.want.Tag || got.Digest != tt.want.Digest { 50 | t.Errorf("ParseImage(%s) got %v, want %v", tt.image, got, tt.want) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/pkg/clouds/do/common.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | type Region string 4 | 5 | func (r Region) String() string { 6 | return string(r) 7 | } 8 | -------------------------------------------------------------------------------- /src/pkg/clouds/do/region/region.go: -------------------------------------------------------------------------------- 1 | package region 2 | 3 | import "github.com/DefangLabs/defang/src/pkg/clouds/do" 4 | 5 | type Region = do.Region 6 | 7 | const ( 8 | AMS2 Region = "ams2" 9 | AMS3 Region = "ams3" 10 | BLR1 Region = "blr1" 11 | FRA1 Region = "fra1" 12 | LON1 Region = "lon1" 13 | NYC1 Region = "nyc1" 14 | NYC2 Region = "nyc2" 15 | NYC3 Region = "nyc3" 16 | SFO1 Region = "sfo1" 17 | SFO2 Region = "sfo2" 18 | SFO3 Region = "sfo3" 19 | SGP1 Region = "sgp1" 20 | SYD1 Region = "syd1" 21 | TOR1 Region = "tor1" 22 | ) 23 | -------------------------------------------------------------------------------- /src/pkg/cmd/destroy.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | func Destroy(ctx context.Context, region Region) error { 8 | driver, err := createDriver(region) 9 | if err != nil { 10 | return err 11 | } 12 | return driver.TearDown(ctx) 13 | } 14 | -------------------------------------------------------------------------------- /src/pkg/cmd/info.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/DefangLabs/defang/src/pkg/types" 8 | ) 9 | 10 | func PrintInfo(ctx context.Context, region Region, id types.TaskID) error { 11 | driver, err := createDriver(region) 12 | if err != nil { 13 | return err 14 | } 15 | info, err := driver.GetInfo(ctx, id) 16 | if err != nil { 17 | return err 18 | } 19 | fmt.Println("IP:", info.IP) 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /src/pkg/cmd/logs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/DefangLabs/defang/src/pkg/types" 7 | ) 8 | 9 | func Logs(ctx context.Context, region Region, id types.TaskID) error { 10 | driver, err := createDriver(region) 11 | if err != nil { 12 | return err 13 | } 14 | return driver.Tail(ctx, id) 15 | } 16 | -------------------------------------------------------------------------------- /src/pkg/cmd/stop.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/DefangLabs/defang/src/pkg/types" 7 | ) 8 | 9 | func Stop(ctx context.Context, region Region, id types.TaskID) error { 10 | driver, err := createDriver(region) 11 | if err != nil { 12 | return err 13 | } 14 | return driver.Stop(ctx, id) 15 | } 16 | -------------------------------------------------------------------------------- /src/pkg/dns/mock.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "context" 5 | "net" 6 | ) 7 | 8 | type DNSRequest struct { 9 | Type string 10 | Domain string 11 | } 12 | 13 | type DNSResponse struct { 14 | Records []string 15 | Error error 16 | } 17 | 18 | type MockResolver struct { 19 | Records map[DNSRequest]DNSResponse 20 | } 21 | 22 | type ErrUnexpectedRequest DNSRequest 23 | 24 | func (e ErrUnexpectedRequest) Error() string { 25 | return "Unexpected request: " + DNSRequest(e).Domain + " " + DNSRequest(e).Type 26 | } 27 | 28 | func (r MockResolver) records(req DNSRequest) ([]string, error) { 29 | res, ok := r.Records[req] 30 | if !ok { 31 | return nil, ErrUnexpectedRequest(req) 32 | } 33 | return res.Records, res.Error 34 | } 35 | 36 | func convert[E any](a []string, f func(string) E) []E { 37 | b := make([]E, len(a)) 38 | for i, v := range a { 39 | b[i] = f(v) 40 | } 41 | return b 42 | } 43 | 44 | func (r MockResolver) LookupIPAddr(ctx context.Context, domain string) ([]net.IPAddr, error) { 45 | ips, err := r.records(DNSRequest{Type: "A", Domain: domain}) 46 | return convert(ips, func(ip string) net.IPAddr { return net.IPAddr{IP: net.ParseIP(ip)} }), err 47 | } 48 | func (r MockResolver) LookupCNAME(ctx context.Context, domain string) (string, error) { 49 | cnames, err := r.records(DNSRequest{Type: "CNAME", Domain: domain}) 50 | if err != nil { 51 | return "", err 52 | } 53 | return cnames[0], nil 54 | } 55 | func (r MockResolver) LookupNS(ctx context.Context, domain string) ([]*net.NS, error) { 56 | ns, err := r.records(DNSRequest{Type: "NS", Domain: domain}) 57 | return convert(ns, func(n string) *net.NS { return &net.NS{Host: n} }), err 58 | } 59 | -------------------------------------------------------------------------------- /src/pkg/dns/utils.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import "strings" 4 | 5 | func SafeLabel(fqn string) string { 6 | return strings.ReplaceAll(strings.ToLower(fqn), ".", "-") 7 | } 8 | 9 | func Normalize(domain string) string { 10 | return strings.ToLower(strings.TrimSuffix(domain, ".")) 11 | } 12 | -------------------------------------------------------------------------------- /src/pkg/dns/utils_test.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import "testing" 4 | 5 | func TestSafeLabel(t *testing.T) { 6 | tests := []struct { 7 | input string 8 | expected string 9 | }{ 10 | {"example.project", "example-project"}, 11 | {"EXAMPLE.PROJECT", "example-project"}, 12 | {"example.project.", "example-project-"}, 13 | } 14 | 15 | for _, test := range tests { 16 | result := SafeLabel(test.input) 17 | if result != test.expected { 18 | t.Errorf("SafeLabel(%q) = %q; want %q", test.input, result, test.expected) 19 | } 20 | } 21 | } 22 | 23 | func TestNormalize(t *testing.T) { 24 | tests := []struct { 25 | input string 26 | expected string 27 | }{ 28 | {"example.com", "example.com"}, 29 | {"EXAMPLE.COM", "example.com"}, 30 | {"example.com.", "example.com"}, 31 | } 32 | 33 | for _, test := range tests { 34 | result := Normalize(test.input) 35 | if result != test.expected { 36 | t.Errorf("Normalize(%q) = %q; want %q", test.input, result, test.expected) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/pkg/docker/common.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/DefangLabs/defang/src/pkg/types" 8 | "github.com/docker/docker/client" 9 | ) 10 | 11 | type ContainerID = types.TaskID 12 | 13 | type Docker struct { 14 | *client.Client 15 | 16 | image string 17 | memory uint64 18 | platform string 19 | } 20 | 21 | func New() *Docker { 22 | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | return &Docker{ 28 | Client: cli, 29 | } 30 | } 31 | 32 | var _ types.Driver = (*Docker)(nil) 33 | 34 | func (Docker) PutSecret(ctx context.Context, name, value string) error { 35 | return errors.New("docker does not support secrets") 36 | } 37 | 38 | func (Docker) ListSecrets(ctx context.Context) ([]string, error) { 39 | return nil, errors.New("docker does not support secrets") 40 | } 41 | 42 | func (Docker) CreateUploadURL(ctx context.Context, name string) (string, error) { 43 | return "", errors.New("docker does not support uploads") 44 | } 45 | -------------------------------------------------------------------------------- /src/pkg/docker/info.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/DefangLabs/defang/src/pkg/types" 7 | ) 8 | 9 | func (d Docker) GetInfo(ctx context.Context, id ContainerID) (*types.TaskInfo, error) { 10 | info, err := d.ContainerInspect(ctx, *id) 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | // b, _ := json.MarshalIndent(info, "", " ") 16 | // println(string(b)) 17 | 18 | for _, mapping := range info.NetworkSettings.Ports { 19 | // TODO: add port 20 | // return "Host IP: " + mapping[0].HostIP + ":" + mapping[0].HostPort, nil 21 | return &types.TaskInfo{IP: mapping[0].HostIP}, nil 22 | } 23 | 24 | return nil, nil 25 | } 26 | -------------------------------------------------------------------------------- /src/pkg/docker/run.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/docker/docker/api/types" 8 | "github.com/docker/docker/api/types/container" 9 | v1 "github.com/opencontainers/image-spec/specs-go/v1" 10 | ) 11 | 12 | func (d Docker) Run(ctx context.Context, env map[string]string, cmd ...string) (ContainerID, error) { 13 | resp, err := d.ContainerCreate(ctx, &container.Config{ 14 | Image: d.image, 15 | Env: mapToSlice(env), 16 | Cmd: cmd, 17 | }, &container.HostConfig{ 18 | AutoRemove: true, // --rm; FIXME: this causes "No such container" if the container exits early 19 | PublishAllPorts: true, // -P 20 | Resources: container.Resources{ 21 | Memory: int64(d.memory), 22 | }, 23 | }, nil, parsePlatform(d.platform), "") 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return &resp.ID, d.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}) 29 | } 30 | 31 | func mapToSlice(m map[string]string) []string { 32 | s := make([]string, 0, len(m)) 33 | for k, v := range m { 34 | // Ensure no = in key 35 | if strings.ContainsRune(k, '=') { 36 | panic("invalid environment variable key") 37 | } 38 | s = append(s, k+"="+v) 39 | } 40 | return s 41 | } 42 | 43 | func parsePlatform(platform string) *v1.Platform { 44 | parts := strings.Split(platform, "/") 45 | var p = &v1.Platform{} 46 | switch len(parts) { 47 | case 3: 48 | p.Variant = parts[2] 49 | fallthrough 50 | case 2: 51 | p.OS = parts[0] 52 | p.Architecture = parts[1] 53 | case 1: 54 | p.Architecture = parts[0] 55 | default: 56 | panic("invalid platform: " + platform) 57 | } 58 | return p 59 | } 60 | -------------------------------------------------------------------------------- /src/pkg/docker/setup.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "os" 8 | 9 | "github.com/docker/docker/api/types" 10 | 11 | pkgtypes "github.com/DefangLabs/defang/src/pkg/types" 12 | ) 13 | 14 | func (d *Docker) SetUp(ctx context.Context, containers []pkgtypes.Container) error { 15 | if len(containers) != 1 { 16 | return errors.New("only one container is supported with docker driver") 17 | } 18 | task := containers[0] 19 | rc, err := d.ImagePull(ctx, task.Image, types.ImagePullOptions{Platform: task.Platform}) 20 | if err != nil { 21 | return err 22 | } 23 | defer rc.Close() 24 | _, err = io.Copy(contextAwareWriter{ctx, os.Stderr}, rc) // FIXME: this outputs JSON to stderr 25 | d.image = task.Image 26 | d.memory = task.Memory 27 | d.platform = task.Platform 28 | return err 29 | } 30 | 31 | type contextAwareWriter struct { 32 | ctx context.Context 33 | io.Writer 34 | } 35 | 36 | func (cw contextAwareWriter) Write(p []byte) (n int, err error) { 37 | select { 38 | case <-cw.ctx.Done(): // Detect context cancelation 39 | return 0, cw.ctx.Err() 40 | default: 41 | return cw.Writer.Write(p) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/pkg/docker/stop.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/docker/docker/api/types/container" 7 | ) 8 | 9 | func (d Docker) Stop(ctx context.Context, id ContainerID) error { 10 | return d.ContainerStop(ctx, *id, container.StopOptions{}) 11 | } 12 | -------------------------------------------------------------------------------- /src/pkg/docker/tail.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/docker/docker/api/types" 8 | "github.com/docker/docker/pkg/stdcopy" 9 | ) 10 | 11 | func (d Docker) Tail(ctx context.Context, id ContainerID) error { 12 | rc, err := d.Client.ContainerLogs(ctx, *id, types.ContainerLogsOptions{ 13 | Follow: true, 14 | ShowStderr: true, 15 | ShowStdout: true, 16 | }) 17 | if err != nil { 18 | return err 19 | } 20 | defer rc.Close() 21 | _, err = stdcopy.StdCopy(os.Stdout, os.Stderr, rc) 22 | return err 23 | } 24 | -------------------------------------------------------------------------------- /src/pkg/docker/teardown.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/DefangLabs/defang/src/pkg/cli/client" 7 | ) 8 | 9 | func (d Docker) TearDown(ctx context.Context) error { 10 | return client.ErrNotImplemented("not implemented") 11 | } 12 | -------------------------------------------------------------------------------- /src/pkg/github/id_token.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/DefangLabs/defang/src/pkg/http" 11 | ) 12 | 13 | // GitHub OIDC docs: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect 14 | 15 | type actionsIdTokenResponse struct { 16 | Count int `json:"count"` 17 | Value string `json:"value"` 18 | } 19 | 20 | func GetIdToken(ctx context.Context) (string, error) { 21 | requestUrl := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL") 22 | requestToken := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN") 23 | if requestUrl == "" || requestToken == "" { 24 | return "", errors.New("ACTIONS_ID_TOKEN_REQUEST_URL or ACTIONS_ID_TOKEN_REQUEST_TOKEN not set") 25 | } 26 | 27 | // TODO: append &audience=… to specify the audience 28 | resp, err := http.GetWithAuth(ctx, requestUrl, "Bearer "+requestToken) 29 | if err != nil { 30 | return "", err 31 | } 32 | defer resp.Body.Close() 33 | if resp.StatusCode != 200 { 34 | return "", fmt.Errorf("HTTP %d", resp.StatusCode) 35 | } 36 | 37 | var actionsIdTokenResponse actionsIdTokenResponse 38 | if err := json.NewDecoder(resp.Body).Decode(&actionsIdTokenResponse); err != nil { 39 | return "", err 40 | } 41 | return actionsIdTokenResponse.Value, nil 42 | } 43 | -------------------------------------------------------------------------------- /src/pkg/github/id_token_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestGetIdToken(t *testing.T) { 10 | requestUrl := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL") 11 | if requestUrl == "" { 12 | t.Skip("ACTIONS_ID_TOKEN_REQUEST_URL not set") 13 | } 14 | 15 | jwt, err := GetIdToken(context.Background()) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | if jwt == "" { 20 | t.Error("empty jwt") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/pkg/http/client.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | 7 | "github.com/hashicorp/go-retryablehttp" 8 | ) 9 | 10 | var DefaultClient = newClient().StandardClient() 11 | 12 | type slogLogger struct{} 13 | 14 | func (slogLogger) Printf(format string, args ...interface{}) { 15 | slog.Debug(fmt.Sprintf(format, args...)) 16 | } 17 | 18 | func newClient() *retryablehttp.Client { 19 | c := retryablehttp.NewClient() // default client retries 4 times: 0+1+2+4+8 = 15s max 20 | c.Logger = slogLogger{} 21 | return c 22 | } 23 | -------------------------------------------------------------------------------- /src/pkg/http/get.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | type Header = http.Header 9 | 10 | func GetWithContext(ctx context.Context, url string) (*http.Response, error) { 11 | hreq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 12 | if err != nil { 13 | return nil, err 14 | } 15 | return DefaultClient.Do(hreq) 16 | } 17 | 18 | func GetWithHeader(ctx context.Context, url string, header http.Header) (*http.Response, error) { 19 | hreq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 20 | if err != nil { 21 | return nil, err 22 | } 23 | hreq.Header = header 24 | return DefaultClient.Do(hreq) 25 | } 26 | 27 | func GetWithAuth(ctx context.Context, url, auth string) (*http.Response, error) { 28 | return GetWithHeader(ctx, url, http.Header{"Authorization": []string{auth}}) 29 | } 30 | -------------------------------------------------------------------------------- /src/pkg/http/post.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | ) 11 | 12 | // PostForValues issues a POST to the specified URL and returns the response body as url.Values. 13 | func PostForValues(_url, contentType string, body io.Reader) (url.Values, error) { 14 | resp, err := DefaultClient.Post(_url, contentType, body) 15 | if err != nil { 16 | return nil, err 17 | } 18 | defer resp.Body.Close() 19 | bytes, err := io.ReadAll(resp.Body) 20 | if err != nil { 21 | return nil, err 22 | } 23 | values, err := url.ParseQuery(string(bytes)) 24 | // By default, HTTP status codes in the 2xx range are considered successful 25 | // and the default client will have followed any redirects. 26 | if resp.StatusCode < 200 || resp.StatusCode >= 300 { 27 | return values, fmt.Errorf("unexpected status code: %s", resp.Status) 28 | } 29 | if err != nil { 30 | return nil, fmt.Errorf("failed to parse response body: %w", err) 31 | } 32 | return values, nil 33 | } 34 | 35 | func PostFormWithContext(ctx context.Context, url string, data url.Values) (*http.Response, error) { 36 | return PostWithContext(ctx, url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) 37 | } 38 | 39 | func PostWithContext(ctx context.Context, url, contentType string, body io.Reader) (*http.Response, error) { 40 | hreq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) 41 | if err != nil { 42 | return nil, err 43 | } 44 | hreq.Header.Set("Content-Type", contentType) 45 | return DefaultClient.Do(hreq) 46 | } 47 | -------------------------------------------------------------------------------- /src/pkg/http/post_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestPostForValues(t *testing.T) { 10 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 11 | // Return an error to test the error handling on /error 12 | if r.URL.Path == "/error" { 13 | http.Error(w, "error", http.StatusNotFound) // not retried 14 | return 15 | } 16 | // Return a redirect to test the error handling on unexpected status codes 17 | if r.URL.Path == "/redirect" { 18 | http.Redirect(w, r, "/", http.StatusFound) 19 | return 20 | } 21 | w.Write([]byte("foo=bar&baz=qux")) 22 | })) 23 | t.Cleanup(server.Close) 24 | 25 | t.Run("success", func(t *testing.T) { 26 | values, err := PostForValues(server.URL, "application/text", nil) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | if got, want := values.Get("foo"), "bar"; got != want { 31 | t.Errorf("got %q, want %q", got, want) 32 | } 33 | }) 34 | 35 | t.Run("redirect", func(t *testing.T) { 36 | values, err := PostForValues(server.URL+"/redirect", "application/text", nil) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | if got, want := values.Get("foo"), "bar"; got != want { 41 | t.Errorf("got %q, want %q", got, want) 42 | } 43 | }) 44 | 45 | t.Run("error", func(t *testing.T) { 46 | _, err := PostForValues(server.URL+"/error", "application/text", nil) 47 | if err == nil { 48 | t.Fatal("expected an error") 49 | } 50 | if got, want := err.Error(), "unexpected status code: 404 Not Found"; got != want { 51 | t.Errorf("got %q, want %q", got, want) 52 | } 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /src/pkg/http/put.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | // Put issues a PUT to the specified URL. 10 | // 11 | // Caller should close resp.Body when done reading from it. 12 | // 13 | // If the provided body is an io.Closer, it is closed after the 14 | // request. 15 | // 16 | // To set custom headers, use NewRequest and DefaultClient.Do. 17 | // 18 | // See the Client.Do method documentation for details on how redirects 19 | // are handled. 20 | func Put(ctx context.Context, url string, contentType string, body io.Reader) (*http.Response, error) { 21 | req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, body) 22 | if err != nil { 23 | return nil, err 24 | } 25 | req.Header.Set("Content-Type", contentType) 26 | return DefaultClient.Do(req) 27 | } 28 | -------------------------------------------------------------------------------- /src/pkg/http/put_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestPutRetries(t *testing.T) { 13 | const body = "test" 14 | calls := 0 15 | 16 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 | calls++ 18 | if calls < 3 { 19 | http.Error(w, "error", http.StatusInternalServerError) 20 | return 21 | } 22 | w.WriteHeader(http.StatusOK) 23 | if b, err := io.ReadAll(r.Body); err != nil || string(b) != body { 24 | t.Error("expected body to be read") 25 | } 26 | })) 27 | t.Cleanup(server.Close) 28 | 29 | resp, err := Put(context.Background(), server.URL, "text/plain", strings.NewReader(body)) 30 | if err != nil { 31 | t.Fatalf("unexpected error: %v", err) 32 | } 33 | defer resp.Body.Close() 34 | if calls != 3 { 35 | t.Errorf("expected 3 calls, got %d", calls) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/pkg/http/query.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import "net/url" 4 | 5 | func RemoveQueryParam(qurl string) string { 6 | u, err := url.Parse(qurl) 7 | if err != nil { 8 | return qurl 9 | } 10 | u.RawQuery = "" 11 | return u.String() 12 | } 13 | -------------------------------------------------------------------------------- /src/pkg/http/query_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import "testing" 4 | 5 | func TestRemoveQueryParam(t *testing.T) { 6 | url := "https://example.com/foo?bar=baz" 7 | expected := "https://example.com/foo" 8 | actual := RemoveQueryParam(url) 9 | if actual != expected { 10 | t.Errorf("expected %q, got %q", expected, actual) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/pkg/local/local_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package local 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | "time" 10 | 11 | "github.com/DefangLabs/defang/src/pkg/types" 12 | ) 13 | 14 | func TestLocal(t *testing.T) { 15 | l := New() 16 | ctx := context.Background() 17 | 18 | t.Run("SetUp", func(t *testing.T) { 19 | if err := l.SetUp(ctx, []types.Container{{EntryPoint: []string{"/bin/sh"}}}); err != nil { 20 | t.Fatal(err) 21 | } 22 | }) 23 | defer l.TearDown(ctx) 24 | 25 | var pid PID 26 | t.Run("Run", func(t *testing.T) { 27 | env := map[string]string{"FOO": "bar"} 28 | var err error 29 | pid, err = l.Run(ctx, env, "-c", "sleep 1 ; echo $FOO") 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | }) 34 | 35 | t.Run("Tail", func(t *testing.T) { 36 | ctx, cancel := context.WithTimeout(ctx, 10*time.Second) 37 | defer cancel() 38 | // This should print "bar" to stdout 39 | if err := l.Tail(ctx, pid); err != nil { 40 | t.Error(err) 41 | } 42 | }) 43 | 44 | t.Run("TearDown", func(t *testing.T) { 45 | if err := l.TearDown(ctx); err != nil { 46 | t.Error(err) 47 | } 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /src/pkg/logs/fluent.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | type Source string 4 | 5 | const ( 6 | SourceStdout Source = "stdout" 7 | SourceStderr Source = "stderr" 8 | 9 | ErrorPrefix = " ** " 10 | ) 11 | 12 | type FirelensMessage struct { 13 | Log string `json:"log"` 14 | ContainerID string `json:"container_id,omitempty"` 15 | ContainerName string `json:"container_name,omitempty"` 16 | Source Source `json:"source,omitempty"` // "stdout" or "stderr" 17 | EcsTaskDefinition string `json:"ecs_task_definition,omitempty"` // ECS metadata 18 | EcsTaskArn string `json:"ecs_task_arn,omitempty"` // ECS metadata 19 | EcsCluster string `json:"ecs_cluster,omitempty"` // ECS metadata 20 | Etag string `json:"etag,omitempty"` // added by us 21 | Host string `json:"host,omitempty"` // added by us 22 | Service string `json:"service,omitempty"` // added by us 23 | } 24 | -------------------------------------------------------------------------------- /src/pkg/logs/logrus.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/DefangLabs/defang/src/pkg" 8 | "github.com/DefangLabs/defang/src/pkg/term" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func IsLogrusError(message string) bool { 13 | // Logrus's TextFormatter prefixes messages with the uppercase log level, optionally truncated and/or in color 14 | switch message[:pkg.Min(len(message), 4)] { 15 | case "ERRO", "FATA", "PANI", "\x1b[31": // red 16 | return true // always show 17 | case "WARN", "\x1b[33": // yellow 18 | fallthrough 19 | case "TRAC", "DEBU", "\x1b[37": // gray 20 | fallthrough 21 | case "", "INFO", "\x1b[36": // blue 22 | return false // only shown with --verbose 23 | default: 24 | return true // show by default (likely Dockerfile errors) 25 | } 26 | } 27 | 28 | type TermLogFormatter struct { 29 | Term *term.Term 30 | } 31 | 32 | func (f TermLogFormatter) Format(entry *logrus.Entry) ([]byte, error) { 33 | var buf strings.Builder 34 | buf.WriteString(entry.Message) 35 | for k, v := range entry.Data { 36 | fmt.Fprintf(&buf, " %s=%v", k, v) 37 | } 38 | 39 | switch entry.Level { 40 | case logrus.PanicLevel, logrus.FatalLevel: 41 | f.Term.Fatal(buf.String()) 42 | case logrus.ErrorLevel: 43 | f.Term.Error(buf.String()) 44 | case logrus.WarnLevel: 45 | f.Term.Warn(buf.String()) 46 | case logrus.InfoLevel: 47 | f.Term.Info(buf.String()) 48 | case logrus.DebugLevel, logrus.TraceLevel: 49 | f.Term.Debug(buf.String()) 50 | } 51 | 52 | return nil, nil 53 | } 54 | 55 | type DiscardFormatter struct{} 56 | 57 | func (f DiscardFormatter) Format(entry *logrus.Entry) ([]byte, error) { 58 | return nil, nil 59 | } 60 | -------------------------------------------------------------------------------- /src/pkg/mcp/deployment_info/deployment_info.go: -------------------------------------------------------------------------------- 1 | package deployment_info 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/DefangLabs/defang/src/pkg/cli/client" 8 | "github.com/DefangLabs/defang/src/pkg/term" 9 | defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" 10 | ) 11 | 12 | type ErrNoServices struct { 13 | ProjectName string // may be empty 14 | } 15 | 16 | func (e ErrNoServices) Error() string { 17 | return fmt.Sprintf("no services found in project %q", e.ProjectName) 18 | } 19 | 20 | type Service struct { 21 | Service string 22 | DeploymentId string 23 | PublicFqdn string 24 | PrivateFqdn string 25 | Status string 26 | } 27 | 28 | func GetServices(ctx context.Context, projectName string, provider client.Provider) ([]Service, error) { 29 | term.Infof("Listing services in project %q", projectName) 30 | 31 | getServicesResponse, err := provider.GetServices(ctx, &defangv1.GetServicesRequest{Project: projectName}) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | numServices := len(getServicesResponse.Services) 37 | 38 | if numServices == 0 { 39 | return nil, ErrNoServices{ProjectName: projectName} 40 | } 41 | 42 | result := make([]Service, numServices) 43 | for i, si := range getServicesResponse.Services { 44 | result[i] = Service{ 45 | Service: si.Service.Name, 46 | DeploymentId: si.Etag, 47 | PublicFqdn: si.PublicFqdn, 48 | PrivateFqdn: si.PrivateFqdn, 49 | Status: si.Status, 50 | } 51 | } 52 | 53 | return result, nil 54 | } 55 | -------------------------------------------------------------------------------- /src/pkg/mcp/tools/login.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | 7 | "github.com/DefangLabs/defang/src/pkg/cli" 8 | "github.com/DefangLabs/defang/src/pkg/term" 9 | "github.com/DefangLabs/defang/src/pkg/track" 10 | "github.com/mark3labs/mcp-go/mcp" 11 | "github.com/mark3labs/mcp-go/server" 12 | ) 13 | 14 | // setupLoginTool configures and adds the login tool to the MCP server 15 | func setupLoginTool(s *server.MCPServer, cluster string, authPort int) { 16 | term.Info("Creating login tool") 17 | loginTool := mcp.NewTool("login", 18 | mcp.WithDescription("Login to Defang"), 19 | ) 20 | term.Debug("Login tool created") 21 | 22 | // Add the login tool handler - make it non-blocking 23 | term.Info("Adding login tool handler") 24 | s.AddTool(loginTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 25 | term.Infof("Login tool called") 26 | track.Evt("MCP Login Tool") 27 | 28 | client, err := cli.Connect(ctx, cluster) 29 | if err != nil { 30 | if authPort != 0 { 31 | return mcp.NewToolResultText("Please open this URL in your browser: http://127.0.0.1:" + strconv.Itoa(authPort) + " to login"), nil 32 | } 33 | err = cli.InteractiveLoginPrompt(ctx, client, cluster) 34 | if err != nil { 35 | return mcp.NewToolResultErrorFromErr("Failed to login", err), nil 36 | } 37 | } 38 | 39 | output := "Successfully logged in to Defang" 40 | 41 | term.Info(output) 42 | return mcp.NewToolResultText(output), nil 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /src/pkg/mcp/tools/tools.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "github.com/DefangLabs/defang/src/pkg/term" 5 | "github.com/mark3labs/mcp-go/server" 6 | ) 7 | 8 | // SetupTools configures and adds all the MCP tools to the server 9 | func SetupTools(s *server.MCPServer, cluster string, authPort int) { 10 | // Create a tool for logging in and getting a new token 11 | term.Info("Setting up login tool") 12 | setupLoginTool(s, cluster, authPort) 13 | 14 | // Create a tool for listing services 15 | term.Info("Setting up services tool") 16 | setupServicesTool(s, cluster) 17 | 18 | // Create a tool for deployment 19 | term.Info("Setting up deployment tool") 20 | setupDeployTool(s, cluster) 21 | 22 | // Create a tool for destroying services 23 | term.Info("Setting up destroy tool") 24 | setupDestroyTool(s, cluster) 25 | 26 | term.Info("All MCP tools have been set up successfully") 27 | } 28 | -------------------------------------------------------------------------------- /src/pkg/money/money.go: -------------------------------------------------------------------------------- 1 | package money 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strings" 7 | 8 | google "github.com/DefangLabs/defang/src/protos/google/type" 9 | ) 10 | 11 | type Money google.Money 12 | 13 | // Currency symbol map (expand as needed) 14 | var currencySymbols = map[string]string{ 15 | "USD": "$", 16 | "EUR": "€", 17 | "GBP": "£", 18 | "JPY": "¥", 19 | // Add more as needed 20 | } 21 | 22 | func NewMoney(amount float64, currency string) *Money { 23 | units := int64(amount) 24 | nanos := int32(math.Round((amount - float64(units)) * 1e9)) 25 | 26 | // Fix signs: if nanos and units mismatch, adjust accordingly 27 | if amount < 0 && nanos > 0 { 28 | units-- 29 | nanos = nanos - 1e9 30 | } 31 | if amount > 0 && nanos < 0 { 32 | units++ 33 | nanos = nanos + 1e9 34 | } 35 | 36 | return &Money{ 37 | CurrencyCode: currency, 38 | Units: units, 39 | Nanos: nanos, 40 | } 41 | } 42 | 43 | func (m *Money) String() string { 44 | symbol, ok := currencySymbols[strings.ToUpper(m.CurrencyCode)] 45 | if !ok { 46 | symbol = m.CurrencyCode + " " 47 | } 48 | 49 | // Combine units and nanos 50 | total := float64(m.Units) + float64(m.Nanos)/1e9 51 | if total < 0 { 52 | return fmt.Sprintf("-%s%.2f", symbol, -total) 53 | } 54 | return fmt.Sprintf("%s%.2f", symbol, total) 55 | } 56 | -------------------------------------------------------------------------------- /src/pkg/money/money_test.go: -------------------------------------------------------------------------------- 1 | package money 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMoney(t *testing.T) { 8 | tests := []struct { 9 | amount float64 10 | currency string 11 | expected string 12 | }{ 13 | {1.23, "USD", "$1.23"}, 14 | {1.23, "GBP", "£1.23"}, 15 | {0, "USD", "$0.00"}, 16 | {-1.23, "USD", "-$1.23"}, 17 | } 18 | 19 | for _, test := range tests { 20 | m := NewMoney(test.amount, test.currency) 21 | if m.String() != test.expected { 22 | t.Errorf("Expected %s but got %s", test.expected, m.String()) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/pkg/quota/quota.go: -------------------------------------------------------------------------------- 1 | package quota 2 | 3 | type Quotas struct { 4 | ServiceQuotas 5 | ConfigCount int 6 | ConfigSize int 7 | Ingress int 8 | Services int 9 | } 10 | -------------------------------------------------------------------------------- /src/pkg/scope/scope.go: -------------------------------------------------------------------------------- 1 | package scope 2 | 3 | type Scope string 4 | 5 | const ( 6 | Admin Scope = "admin" 7 | Any Scope = "" // used for matching any scope 8 | Delete Scope = "delete" 9 | Read Scope = "read" 10 | Tail Scope = "tail" 11 | ) 12 | 13 | func (s Scope) String() string { 14 | return string(s) 15 | } 16 | 17 | func All() []Scope { 18 | return []Scope{Admin, Delete, Read, Tail} 19 | } 20 | -------------------------------------------------------------------------------- /src/pkg/spinner/spinner.go: -------------------------------------------------------------------------------- 1 | package spinner 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | var SpinnerChars = `-\|/` 10 | 11 | type Spinner struct { 12 | cnt int 13 | } 14 | 15 | func New() *Spinner { 16 | return &Spinner{} 17 | } 18 | 19 | func (s *Spinner) Next() string { 20 | s.cnt++ 21 | runes := []rune(SpinnerChars) 22 | return string([]rune{runes[s.cnt%len(runes)], '\b'}) 23 | } 24 | 25 | func (s *Spinner) Start(ctx context.Context) context.CancelFunc { 26 | cancelCtx, cancel := context.WithCancel(ctx) 27 | go func(spinnerCtx context.Context) { 28 | ticker := time.NewTicker(250 * time.Millisecond) 29 | defer ticker.Stop() 30 | for { 31 | select { 32 | case <-spinnerCtx.Done(): 33 | return 34 | case <-ticker.C: 35 | fmt.Print(s.Next()) 36 | } 37 | } 38 | }(cancelCtx) 39 | 40 | return cancel 41 | } 42 | -------------------------------------------------------------------------------- /src/pkg/term/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /src/pkg/term/colorizer_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package term 5 | 6 | import "os" 7 | 8 | func EnableANSI() func() { 9 | return func() {} 10 | } 11 | 12 | func hasTermInEnv() bool { 13 | return os.Getenv("TERM") != "" 14 | } 15 | -------------------------------------------------------------------------------- /src/pkg/term/colorizer_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package term 5 | 6 | import ( 7 | "github.com/muesli/termenv" 8 | ) 9 | 10 | func EnableANSI() func() { 11 | mode, err := termenv.EnableWindowsANSIConsole() 12 | if err != nil { 13 | return func() {} 14 | } 15 | return func() { 16 | termenv.RestoreWindowsConsole(mode) 17 | } 18 | } 19 | 20 | func hasTermInEnv() bool { 21 | return true 22 | } 23 | -------------------------------------------------------------------------------- /src/pkg/term/input.go: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | 8 | "github.com/ross96D/cancelreader" 9 | ) 10 | 11 | var ErrClosed = errors.New("closed") 12 | 13 | type nonBlockingStdin struct { 14 | cancelreader.CancelReader 15 | } 16 | 17 | func (n *nonBlockingStdin) Close() error { 18 | if !n.CancelReader.Cancel() { 19 | // Could not cancel; try closing the underlying handle 20 | if err := os.Stdin.Close(); err != nil { 21 | return err 22 | } 23 | return ErrClosed 24 | } 25 | return nil 26 | } 27 | 28 | func NewNonBlockingStdin() io.ReadCloser { 29 | cr, err := cancelreader.NewReader(os.Stdin) 30 | if err != nil { 31 | return os.Stdin 32 | } 33 | return &nonBlockingStdin{cr} 34 | } 35 | -------------------------------------------------------------------------------- /src/pkg/term/test_utils.go: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func SetupTestTerm(t *testing.T) (*bytes.Buffer, *bytes.Buffer) { 10 | t.Helper() 11 | 12 | var stdout, stderr bytes.Buffer 13 | testTerm := NewTerm(os.Stdin, &stdout, &stderr) 14 | testTerm.ForceColor(true) 15 | oldTerm := DefaultTerm 16 | DefaultTerm = testTerm 17 | t.Cleanup(func() { 18 | DefaultTerm = oldTerm 19 | }) 20 | 21 | return &stdout, &stderr 22 | } 23 | -------------------------------------------------------------------------------- /src/pkg/term/unbuf.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package term provides support functions for dealing with terminals, as 6 | // commonly found on UNIX systems. 7 | // 8 | // Putting a terminal into raw mode is the most common requirement: 9 | // 10 | // oldState, err := term.MakeRaw(int(os.Stdin.Fd())) 11 | // if err != nil { 12 | // panic(err) 13 | // } 14 | // defer term.Restore(int(os.Stdin.Fd()), oldState) 15 | // 16 | // Note that on non-Unix systems os.Stdin.Fd() may not be 0 17 | package term 18 | 19 | // State contains the state of a terminal. 20 | type State struct { 21 | state 22 | } 23 | 24 | // MakeRaw puts the terminal connected to the given file descriptor into raw 25 | // mode and returns the previous state of the terminal so that it can be 26 | // restored. 27 | func MakeUnbuf(fd int) (*State, error) { 28 | return makeUnbuf(fd) 29 | } 30 | 31 | // Restore restores the terminal connected to the given file descriptor to a 32 | // previous state. 33 | func Restore(fd int, oldState *State) error { 34 | return restore(fd, oldState) 35 | } 36 | -------------------------------------------------------------------------------- /src/pkg/term/unbuf_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos 6 | 7 | package term 8 | 9 | import ( 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | type state struct { 14 | termios unix.Termios 15 | } 16 | 17 | func makeUnbuf(fd int) (*State, error) { 18 | termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | oldState := State{state{termios: *termios}} 24 | 25 | // This is slightly different than term.MakeRaw, because we don't want to change the output processing. 26 | termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON 27 | // termios.Oflag &^= unix.OPOST 28 | termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON //| unix.ISIG | unix.IEXTEN 29 | termios.Cflag &^= unix.CSIZE | unix.PARENB 30 | termios.Cflag |= unix.CS8 31 | termios.Cc[unix.VMIN] = 1 32 | termios.Cc[unix.VTIME] = 0 33 | if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil { 34 | return nil, err 35 | } 36 | 37 | return &oldState, nil 38 | } 39 | 40 | func restore(fd int, state *State) error { 41 | return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios) 42 | } 43 | -------------------------------------------------------------------------------- /src/pkg/term/unbuf_unix_bsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build darwin || dragonfly || freebsd || netbsd || openbsd 6 | 7 | package term 8 | 9 | import "golang.org/x/sys/unix" 10 | 11 | const ioctlReadTermios = unix.TIOCGETA 12 | const ioctlWriteTermios = unix.TIOCSETA 13 | -------------------------------------------------------------------------------- /src/pkg/term/unbuf_unix_other.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build aix || linux || solaris || zos 6 | 7 | package term 8 | 9 | import "golang.org/x/sys/unix" 10 | 11 | const ioctlReadTermios = unix.TCGETS 12 | const ioctlWriteTermios = unix.TCSETS 13 | -------------------------------------------------------------------------------- /src/pkg/term/unbuf_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package term 6 | 7 | import ( 8 | "golang.org/x/sys/windows" 9 | ) 10 | 11 | type state struct { 12 | mode uint32 13 | } 14 | 15 | func makeUnbuf(fd int) (*State, error) { 16 | var st uint32 17 | if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { 18 | return nil, err 19 | } 20 | raw := st &^ (windows.ENABLE_ECHO_INPUT | /*windows.ENABLE_PROCESSED_INPUT |*/ windows.ENABLE_LINE_INPUT) 21 | if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil { 22 | return nil, err 23 | } 24 | return &State{state{st}}, nil 25 | } 26 | 27 | func restore(fd int, state *State) error { 28 | return windows.SetConsoleMode(windows.Handle(fd), state.mode) 29 | } 30 | -------------------------------------------------------------------------------- /src/pkg/types/error.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "errors" 4 | 5 | var ErrComposeFileNotFound = errors.New("no compose.yaml file found") 6 | -------------------------------------------------------------------------------- /src/pkg/types/etag.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type ETag = string 4 | -------------------------------------------------------------------------------- /src/pkg/types/tenant.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type TenantName string 4 | 5 | const ( 6 | DEFAULT_TENANT TenantName = "" // the default tenant (GitHub login) 7 | ) 8 | 9 | func (t TenantName) String() string { 10 | if t == DEFAULT_TENANT { 11 | return "default" 12 | } 13 | return string(t) 14 | } 15 | -------------------------------------------------------------------------------- /src/protos/buf.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | breaking: 3 | use: 4 | - FILE 5 | lint: 6 | use: 7 | - BASIC # TODO: Should use `DEFAULT` rules, see: https://buf.build/docs/lint/rules#default 8 | #rpc_allow_google_protobuf_empty_requests: true 9 | #rpc_allow_google_protobuf_empty_responses: true 10 | -------------------------------------------------------------------------------- /src/testdata/.gitignore: -------------------------------------------------------------------------------- 1 | **/*.mismatch 2 | .dockerignore 3 | -------------------------------------------------------------------------------- /src/testdata/Fancy-Proj_Dir/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | echo: 3 | restart: unless-stopped 4 | image: ealen/echo-server 5 | ports: 6 | - target: 80 7 | mode: ingress 8 | healthcheck: 9 | test: ["CMD", "curl", "-f", "http://localhost/"] 10 | profiles: 11 | - donotstart 12 | 13 | secrets: 14 | dummy: 15 | external: true 16 | name: dummy 17 | -------------------------------------------------------------------------------- /src/testdata/Fancy-Proj_Dir/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/testdata/Fancy-Proj_Dir/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: fancy-proj_dir 2 | services: {} 3 | networks: 4 | default: 5 | name: fancy-proj_dir_default 6 | secrets: 7 | dummy: 8 | name: dummy 9 | external: true 10 | -------------------------------------------------------------------------------- /src/testdata/Fancy-Proj_Dir/compose.yaml.warnings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefangLabs/defang/d7a16134ca0af0525d6c81d6b883ecfcb82dc155/src/testdata/Fancy-Proj_Dir/compose.yaml.warnings -------------------------------------------------------------------------------- /src/testdata/alttestproj/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest AS alttestproj 2 | ARG DNS 3 | ENV DNS=$DNS 4 | CMD ["sh", "-c", "while true; do nslookup ${DNS} ; sleep 10 ; done"] 5 | -------------------------------------------------------------------------------- /src/testdata/alttestproj/altcomp.yaml: -------------------------------------------------------------------------------- 1 | name: altcomp 2 | services: 3 | dfnx: 4 | restart: unless-stopped 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | target: alttestproj 9 | args: 10 | DNS: dfnx 11 | deploy: 12 | resources: 13 | limits: 14 | cpus: '0.50' 15 | memory: 512M 16 | ports: 17 | - target: 80 18 | mode: ingress 19 | - target: 1234 20 | # mode: host 21 | secrets: 22 | - dummy 23 | healthcheck: 24 | test: ["CMD", "curl", "-f", "http://localhost/"] 25 | # disable: true 26 | 27 | # dfnx: 28 | # build: 29 | # context: . 30 | # dockerfile: Dockerfile.dfn 31 | # ports: 32 | # - 80 33 | 34 | echo: 35 | image: ealen/echo-server 36 | ports: 37 | - target: 80 38 | mode: ingress 39 | healthcheck: 40 | test: ["CMD", "curl", "-f", "http://localhost/"] 41 | # domainname: echotest.gnafed.click 42 | profiles: 43 | - donotstart 44 | x-defang-dns-role: arn:aws:iam::123456789012:role/ecs-service-role 45 | 46 | secrets: 47 | dummy: 48 | external: true 49 | name: dummy 50 | -------------------------------------------------------------------------------- /src/testdata/alttestproj/compose.yaml: -------------------------------------------------------------------------------- 1 | name: tests 2 | services: 3 | dfnx: 4 | restart: unless-stopped 5 | build: . 6 | deploy: 7 | resources: 8 | limits: 9 | memory: 256M 10 | -------------------------------------------------------------------------------- /src/testdata/alttestproj/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "dfnx": { 3 | "build": { 4 | "context": ".", 5 | "dockerfile": "Dockerfile" 6 | }, 7 | "command": null, 8 | "deploy": { 9 | "resources": { 10 | "limits": { 11 | "memory": "268435456" 12 | } 13 | }, 14 | "placement": {} 15 | }, 16 | "entrypoint": null, 17 | "networks": { 18 | "default": null 19 | }, 20 | "restart": "unless-stopped" 21 | } 22 | } -------------------------------------------------------------------------------- /src/testdata/alttestproj/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: tests 2 | services: 3 | dfnx: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | deploy: 8 | resources: 9 | limits: 10 | memory: "268435456" 11 | networks: 12 | default: null 13 | restart: unless-stopped 14 | networks: 15 | default: 16 | name: tests_default 17 | -------------------------------------------------------------------------------- /src/testdata/alttestproj/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | * Packaging the project files for dfnx at . 2 | -------------------------------------------------------------------------------- /src/testdata/alttestproj/ignored.Dockerfile: -------------------------------------------------------------------------------- 1 | # This file should not be included in the build context 2 | FROM scratch 3 | -------------------------------------------------------------------------------- /src/testdata/build/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE 2 | FROM BASE_IMAGE 3 | -------------------------------------------------------------------------------- /src/testdata/build/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | build1: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | shm_size: 2147483648 7 | target: test 8 | args: 9 | - BASE_IMAGE=nginx 10 | build2: 11 | build: 12 | context: . 13 | dockerfile: Dockerfile 14 | shm_size: 2gb 15 | target: test 16 | args: 17 | BASE_IMAGE: alpine 18 | -------------------------------------------------------------------------------- /src/testdata/build/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "build1": { 3 | "build": { 4 | "context": ".", 5 | "dockerfile": "Dockerfile", 6 | "args": { 7 | "BASE_IMAGE": "nginx" 8 | }, 9 | "target": "test", 10 | "shm_size": "2147483648" 11 | }, 12 | "command": null, 13 | "entrypoint": null, 14 | "networks": { 15 | "default": null 16 | } 17 | }, 18 | "build2": { 19 | "build": { 20 | "context": ".", 21 | "dockerfile": "Dockerfile", 22 | "args": { 23 | "BASE_IMAGE": "alpine" 24 | }, 25 | "target": "test", 26 | "shm_size": "2147483648" 27 | }, 28 | "command": null, 29 | "entrypoint": null, 30 | "networks": { 31 | "default": null 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/testdata/build/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: build 2 | services: 3 | build1: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | args: 8 | BASE_IMAGE: nginx 9 | target: test 10 | shm_size: "2147483648" 11 | networks: 12 | default: null 13 | build2: 14 | build: 15 | context: . 16 | dockerfile: Dockerfile 17 | args: 18 | BASE_IMAGE: alpine 19 | target: test 20 | shm_size: "2147483648" 21 | networks: 22 | default: null 23 | networks: 24 | default: 25 | name: build_default 26 | -------------------------------------------------------------------------------- /src/testdata/build/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "build1": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | ! service "build2": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 3 | * Packaging the project files for build1 at . 4 | * Packaging the project files for build2 at . 5 | -------------------------------------------------------------------------------- /src/testdata/compose-go-warn/compose.yaml: -------------------------------------------------------------------------------- 1 | name: compose-go-warning 2 | services: 3 | echo: 4 | image: ealen/echo-server 5 | ports: 6 | - target: 80 7 | mode: ingress 8 | secrets: 9 | dummy: 10 | external: yes # compose-go gives warning for using `yes` for boolean values 11 | name: dummy 12 | -------------------------------------------------------------------------------- /src/testdata/compose-go-warn/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "echo": { 3 | "command": null, 4 | "entrypoint": null, 5 | "image": "ealen/echo-server", 6 | "networks": { 7 | "default": null 8 | }, 9 | "ports": [ 10 | { 11 | "mode": "ingress", 12 | "target": 80, 13 | "protocol": "tcp", 14 | "app_protocol": "http" 15 | } 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /src/testdata/compose-go-warn/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: compose-go-warning 2 | services: 3 | echo: 4 | image: ealen/echo-server 5 | networks: 6 | default: null 7 | ports: 8 | - mode: ingress 9 | target: 80 10 | protocol: tcp 11 | networks: 12 | default: 13 | name: compose-go-warning_default 14 | secrets: 15 | dummy: 16 | name: dummy 17 | external: true 18 | -------------------------------------------------------------------------------- /src/testdata/compose-go-warn/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! "yes" for boolean is not supported by YAML 1.2, please use `true` 2 | ! service "echo": ingress port without healthcheck defaults to GET / HTTP/1.1 3 | ! service "echo": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 4 | -------------------------------------------------------------------------------- /src/testdata/configdetection/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | configdetection: 3 | image: alpine 4 | environment: 5 | - API_KEY=50m34p1k3y # keyword detector 6 | - AWS_CLIENT_ID=AROA1234567890ABCDEF # aws_client_id detector 7 | - GH_PAT=ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890 # github detector 8 | - HIGH_ENTROPY_STRING=VEfk5vO0Q53VkK_uicor # high_entropy_string detector 9 | - MY_URL=https://user:p455w0rd@example.com # url_password detector 10 | - NOT_SENSITIVE=notsensitive 11 | -------------------------------------------------------------------------------- /src/testdata/configdetection/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "configdetection": { 3 | "command": null, 4 | "entrypoint": null, 5 | "environment": { 6 | "API_KEY": "50m34p1k3y", 7 | "AWS_CLIENT_ID": "AROA1234567890ABCDEF", 8 | "GH_PAT": "ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890", 9 | "HIGH_ENTROPY_STRING": "VEfk5vO0Q53VkK_uicor", 10 | "MY_URL": "https://user:p455w0rd@example.com", 11 | "NOT_SENSITIVE": "notsensitive" 12 | }, 13 | "image": "alpine", 14 | "networks": { 15 | "default": null 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/testdata/configdetection/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: configdetection 2 | services: 3 | configdetection: 4 | environment: 5 | API_KEY: 50m34p1k3y 6 | AWS_CLIENT_ID: AROA1234567890ABCDEF 7 | GH_PAT: ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890 8 | HIGH_ENTROPY_STRING: VEfk5vO0Q53VkK_uicor 9 | MY_URL: https://user:p455w0rd@example.com 10 | NOT_SENSITIVE: notsensitive 11 | image: alpine 12 | networks: 13 | default: null 14 | networks: 15 | default: 16 | name: configdetection_default 17 | -------------------------------------------------------------------------------- /src/testdata/configdetection/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "configdetection": environment "API_KEY" may contain sensitive information; consider using 'defang config set API_KEY' to securely store this value 2 | ! service "configdetection": environment "AWS_CLIENT_ID" may contain sensitive information; consider using 'defang config set AWS_CLIENT_ID' to securely store this value 3 | ! service "configdetection": environment "GH_PAT" may contain sensitive information; consider using 'defang config set GH_PAT' to securely store this value 4 | ! service "configdetection": environment "HIGH_ENTROPY_STRING" may contain sensitive information; consider using 'defang config set HIGH_ENTROPY_STRING' to securely store this value 5 | ! service "configdetection": environment "MY_URL" may contain sensitive information; consider using 'defang config set MY_URL' to securely store this value 6 | ! service "configdetection": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 7 | -------------------------------------------------------------------------------- /src/testdata/configoverride/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | service1: 3 | image: alpine 4 | environment: 5 | - VAR1=service1test # this is the config returned by the mock client 6 | -------------------------------------------------------------------------------- /src/testdata/configoverride/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "service1": { 3 | "command": null, 4 | "entrypoint": null, 5 | "environment": { 6 | "VAR1": null 7 | }, 8 | "image": "alpine", 9 | "networks": { 10 | "default": null 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/testdata/configoverride/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: configoverride 2 | services: 3 | service1: 4 | environment: 5 | VAR1: service1test 6 | image: alpine 7 | networks: 8 | default: null 9 | networks: 10 | default: 11 | name: configoverride_default 12 | -------------------------------------------------------------------------------- /src/testdata/configoverride/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "service1": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | -------------------------------------------------------------------------------- /src/testdata/debugproj/Dockerfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefangLabs/defang/d7a16134ca0af0525d6c81d6b883ecfcb82dc155/src/testdata/debugproj/Dockerfile -------------------------------------------------------------------------------- /src/testdata/debugproj/app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch -------------------------------------------------------------------------------- /src/testdata/debugproj/app/main.js: -------------------------------------------------------------------------------- 1 | // This file should be sent to the debugger -------------------------------------------------------------------------------- /src/testdata/debugproj/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | failing: 3 | build: ./app 4 | ok: 5 | build: . 6 | -------------------------------------------------------------------------------- /src/testdata/debugproj/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "failing": { 3 | "build": { 4 | "context": "./app", 5 | "dockerfile": "Dockerfile" 6 | }, 7 | "command": null, 8 | "entrypoint": null, 9 | "networks": { 10 | "default": null 11 | } 12 | }, 13 | "ok": { 14 | "build": { 15 | "context": ".", 16 | "dockerfile": "Dockerfile" 17 | }, 18 | "command": null, 19 | "entrypoint": null, 20 | "networks": { 21 | "default": null 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/testdata/debugproj/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: debugproj 2 | services: 3 | failing: 4 | build: 5 | context: ./app 6 | dockerfile: Dockerfile 7 | networks: 8 | default: null 9 | ok: 10 | build: 11 | context: . 12 | dockerfile: Dockerfile 13 | networks: 14 | default: null 15 | networks: 16 | default: 17 | name: debugproj_default 18 | -------------------------------------------------------------------------------- /src/testdata/debugproj/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "failing": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | ! service "ok": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 3 | * Packaging the project files for failing at ./app 4 | * Packaging the project files for ok at . 5 | -------------------------------------------------------------------------------- /src/testdata/debugproj/main.py: -------------------------------------------------------------------------------- 1 | # This file should not be sent to the debugger -------------------------------------------------------------------------------- /src/testdata/dependson/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | service1: 3 | image: alpine 4 | depends_on: 5 | - service2 6 | - service3 7 | service2: 8 | image: alpine 9 | depends_on: 10 | - service3 11 | service3: 12 | image: alpine 13 | depends_on: 14 | - service3 15 | -------------------------------------------------------------------------------- /src/testdata/dependson/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "service1": { 3 | "command": null, 4 | "depends_on": { 5 | "service2": { 6 | "condition": "service_started", 7 | "required": true 8 | }, 9 | "service3": { 10 | "condition": "service_started", 11 | "required": true 12 | } 13 | }, 14 | "entrypoint": null, 15 | "image": "alpine", 16 | "networks": { 17 | "default": null 18 | } 19 | }, 20 | "service2": { 21 | "command": null, 22 | "depends_on": { 23 | "service3": { 24 | "condition": "service_started", 25 | "required": true 26 | } 27 | }, 28 | "entrypoint": null, 29 | "image": "alpine", 30 | "networks": { 31 | "default": null 32 | } 33 | }, 34 | "service3": { 35 | "command": null, 36 | "depends_on": { 37 | "service3": { 38 | "condition": "service_started", 39 | "required": true 40 | } 41 | }, 42 | "entrypoint": null, 43 | "image": "alpine", 44 | "networks": { 45 | "default": null 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/testdata/dependson/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: dependson 2 | services: 3 | service1: 4 | depends_on: 5 | service2: 6 | condition: service_started 7 | required: true 8 | service3: 9 | condition: service_started 10 | required: true 11 | image: alpine 12 | networks: 13 | default: null 14 | service2: 15 | depends_on: 16 | service3: 17 | condition: service_started 18 | required: true 19 | image: alpine 20 | networks: 21 | default: null 22 | service3: 23 | depends_on: 24 | service3: 25 | condition: service_started 26 | required: true 27 | image: alpine 28 | networks: 29 | default: null 30 | networks: 31 | default: 32 | name: dependson_default 33 | -------------------------------------------------------------------------------- /src/testdata/dependson/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "service1": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | ! service "service2": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 3 | ! service "service3": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 4 | -------------------------------------------------------------------------------- /src/testdata/empty/compose.yaml: -------------------------------------------------------------------------------- 1 | services: {} 2 | -------------------------------------------------------------------------------- /src/testdata/empty/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/testdata/empty/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: empty 2 | services: {} 3 | -------------------------------------------------------------------------------- /src/testdata/empty/compose.yaml.warnings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefangLabs/defang/d7a16134ca0af0525d6c81d6b883ecfcb82dc155/src/testdata/empty/compose.yaml.warnings -------------------------------------------------------------------------------- /src/testdata/emptyenv/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | ARG ARG1 3 | ENV ARG1=$ARG1 4 | RUN echo $ARG1 5 | CMD echo $ARG1 $ENV1 $ENV2 6 | -------------------------------------------------------------------------------- /src/testdata/emptyenv/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | emptyenv: 3 | build: 4 | args: 5 | - ARG1 6 | - ARG2= 7 | environment: 8 | - ENV1 9 | - ENV2= 10 | -------------------------------------------------------------------------------- /src/testdata/emptyenv/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "emptyenv": { 3 | "build": { 4 | "context": ".", 5 | "dockerfile": "Dockerfile", 6 | "args": { 7 | "ARG2": "" 8 | } 9 | }, 10 | "command": null, 11 | "entrypoint": null, 12 | "environment": { 13 | "ENV1": null, 14 | "ENV2": "" 15 | }, 16 | "networks": { 17 | "default": null 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/testdata/emptyenv/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: emptyenv 2 | services: 3 | emptyenv: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | args: 8 | ARG2: "" 9 | environment: 10 | ENV1: null 11 | ENV2: "" 12 | networks: 13 | default: null 14 | networks: 15 | default: 16 | name: emptyenv_default 17 | -------------------------------------------------------------------------------- /src/testdata/emptyenv/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "emptyenv": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | * Packaging the project files for emptyenv at . 3 | -------------------------------------------------------------------------------- /src/testdata/fixupenv/Dockerfile: -------------------------------------------------------------------------------- 1 | # This file intentionally left blank -------------------------------------------------------------------------------- /src/testdata/fixupenv/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | Mistral: 3 | image: "mistral:latest" 4 | ports: 5 | - mode: host 6 | target: 8000 7 | ui: 8 | image: "ui:latest" 9 | environment: 10 | - "API_URL=http://Mistral:8000" 11 | - "SENSITIVE_DATA" 12 | ingress-service: 13 | image: "somedb:latest" 14 | ports: 15 | - mode: ingress 16 | target: 5432 17 | use-ingress-service: 18 | image: "service:latest" 19 | environment: 20 | - "DB_URL=ingress-service:5432" 21 | env-in-config: 22 | image: "service:latest" 23 | environment: 24 | - "CONFIG1=http://Mistral:8000" 25 | fixup-args: 26 | build: 27 | args: 28 | - "API_URL=http://Mistral:8000" 29 | -------------------------------------------------------------------------------- /src/testdata/fixupenv/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: fixupenv 2 | services: 3 | Mistral: 4 | image: mistral:latest 5 | networks: 6 | default: null 7 | ports: 8 | - mode: host 9 | target: 8000 10 | protocol: tcp 11 | env-in-config: 12 | environment: 13 | CONFIG1: http://Mistral:8000 14 | image: service:latest 15 | networks: 16 | default: null 17 | fixup-args: 18 | build: 19 | context: . 20 | dockerfile: Dockerfile 21 | args: 22 | API_URL: http://Mistral:8000 23 | networks: 24 | default: null 25 | ingress-service: 26 | image: somedb:latest 27 | networks: 28 | default: null 29 | ports: 30 | - mode: ingress 31 | target: 5432 32 | protocol: tcp 33 | ui: 34 | environment: 35 | API_URL: http://Mistral:8000 36 | SENSITIVE_DATA: null 37 | image: ui:latest 38 | networks: 39 | default: null 40 | use-ingress-service: 41 | environment: 42 | DB_URL: ingress-service:5432 43 | image: service:latest 44 | networks: 45 | default: null 46 | networks: 47 | default: 48 | name: fixupenv_default 49 | -------------------------------------------------------------------------------- /src/testdata/gpu/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | mistral: 3 | deploy: 4 | replicas: 1 5 | resources: 6 | reservations: 7 | devices: 8 | - capabilities: ["gpu"] 9 | -------------------------------------------------------------------------------- /src/testdata/gpu/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "mistral": { 3 | "command": null, 4 | "deploy": { 5 | "replicas": 1, 6 | "resources": { 7 | "reservations": { 8 | "devices": [ 9 | { 10 | "capabilities": [ 11 | "gpu" 12 | ], 13 | "count": -1 14 | } 15 | ] 16 | } 17 | }, 18 | "placement": {} 19 | }, 20 | "entrypoint": null, 21 | "networks": { 22 | "default": null 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/testdata/gpu/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: gpu 2 | services: 3 | mistral: 4 | deploy: 5 | replicas: 1 6 | resources: 7 | reservations: 8 | devices: 9 | - capabilities: 10 | - gpu 11 | count: -1 12 | networks: 13 | default: null 14 | networks: 15 | default: 16 | name: gpu_default 17 | -------------------------------------------------------------------------------- /src/testdata/gpu/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "mistral": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | -------------------------------------------------------------------------------- /src/testdata/healthcheck/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | flask1: 3 | image: flask 4 | ports: 5 | - 5000 6 | healthcheck: 7 | test: ["CMD", "python", "-c", "import sys, urllib.request;urllib.request.urlopen(sys.argv[1]).read()", "http://localhost/"] 8 | timeout: 1s 9 | flask2: 10 | image: flask 11 | ports: 12 | - 5000 13 | healthcheck: 14 | test: ["CMD", "python", "-c", "import urllib.request;urllib.request.urlopen('http://127.0.0.1/path').read()"] 15 | interval: 10s # OK to only override interval w/o timeout 16 | wget: 17 | image: alpine 18 | ports: 19 | - 80 20 | healthcheck: 21 | test: ["CMD", "wget", "-q", "--spider", "localhost:80"] 22 | curl: 23 | image: curl 24 | ports: 25 | - 80 26 | healthcheck: 27 | test: ["CMD", "curl", "-f", "localhost"] 28 | cmd-shell: 29 | image: alpine 30 | ports: 31 | - 5000 32 | healthcheck: 33 | test: ["CMD-SHELL", "echo hello", "second line"] 34 | none: 35 | image: alpine 36 | ports: 37 | - 5000 38 | healthcheck: 39 | test: ["NONE", "ignored"] 40 | interval: 1m 41 | retries: 3 42 | timeout: 1s 43 | -------------------------------------------------------------------------------- /src/testdata/healthcheck/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "cmd-shell": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | ! service "curl": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 3 | ! service "flask1": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 4 | ! service "flask2": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 5 | ! service "none": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 6 | ! service "wget": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 7 | -------------------------------------------------------------------------------- /src/testdata/interpolate/compose.dev.yaml: -------------------------------------------------------------------------------- 1 | # This file intentionally left blank 2 | -------------------------------------------------------------------------------- /src/testdata/interpolate/compose.yaml: -------------------------------------------------------------------------------- 1 | include: ["compose.${NODE_ENV-dev}.yaml"] 2 | services: 3 | interpolate: 4 | image: alpine 5 | environment: 6 | - NAME=$COMPOSE_PROJECT_NAME 7 | - BRACED=${COMPOSE_PROJECT_NAME} 8 | - DB=postgres://user:$POSTGRES_PASSWORD@db:5432/db 9 | - ${COMPOSE_PROJECT_NAME}=value 10 | - NOP=abc$$def # FIXME: this should not get resolved in CD 11 | - NOP_BRACED=abc$${def} # FIXME: this should not get resolved in CD 12 | - NODE_ENV=${NODE_ENV} 13 | - PORT=${PORT:-8080} 14 | -------------------------------------------------------------------------------- /src/testdata/interpolate/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "interpolate": { 3 | "command": null, 4 | "entrypoint": null, 5 | "environment": { 6 | "BRACED": "interpolate", 7 | "DB": "postgres://user:${POSTGRES_PASSWORD}@db:5432/db", 8 | "NAME": "interpolate", 9 | "NODE_ENV": "${NODE_ENV}", 10 | "NOP": "abc$def", 11 | "NOP_BRACED": "abc${def}", 12 | "PORT": "8080", 13 | "interpolate": "value" 14 | }, 15 | "image": "alpine", 16 | "networks": { 17 | "default": null 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/testdata/interpolate/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: interpolate 2 | services: 3 | interpolate: 4 | environment: 5 | BRACED: interpolate 6 | DB: postgres://user:${POSTGRES_PASSWORD}@db:5432/db 7 | NAME: interpolate 8 | NODE_ENV: ${NODE_ENV} 9 | NOP: abc$def 10 | NOP_BRACED: abc${def} 11 | PORT: "8080" 12 | interpolate: value 13 | image: alpine 14 | networks: 15 | default: null 16 | networks: 17 | default: 18 | name: interpolate_default 19 | -------------------------------------------------------------------------------- /src/testdata/interpolate/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! Environment variable "NODE_ENV" is not used; add it to `.env` if needed 2 | ! Environment variable "NODE_ENV" is not used; add it to `.env` or it may be resolved from config during deployment 3 | ! service "interpolate": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 4 | missing configs ["NODE_ENV" "POSTGRES_PASSWORD" "def"] (https://docs.defang.io/docs/concepts/configuration) 5 | -------------------------------------------------------------------------------- /src/testdata/llm/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | llm: 3 | x-defang-llm: true 4 | image: "llm:latest" 5 | 6 | gateway-with-ports: 7 | x-defang-llm: true 8 | image: "defang.io/openai-access-gateway:latest" 9 | ports: 10 | - 5678:5678 11 | 12 | gateway-without-ports: 13 | x-defang-llm: true 14 | image: "defang.io/openai-access-gateway:latest" 15 | 16 | alt-repo: 17 | x-defang-llm: true 18 | image: "altrepo.com/openai-access-gateway:latest" 19 | -------------------------------------------------------------------------------- /src/testdata/llm/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "alt-repo": { 3 | "command": null, 4 | "entrypoint": null, 5 | "image": "altrepo.com/openai-access-gateway:latest", 6 | "networks": { 7 | "default": null 8 | }, 9 | "ports": [ 10 | { 11 | "mode": "host", 12 | "target": 80, 13 | "protocol": "tcp" 14 | } 15 | ] 16 | }, 17 | "gateway-with-ports": { 18 | "command": null, 19 | "entrypoint": null, 20 | "image": "defang.io/openai-access-gateway:latest", 21 | "networks": { 22 | "default": null 23 | }, 24 | "ports": [ 25 | { 26 | "mode": "ingress", 27 | "target": 5678, 28 | "published": "5678", 29 | "protocol": "tcp", 30 | "app_protocol": "http" 31 | } 32 | ] 33 | }, 34 | "gateway-without-ports": { 35 | "command": null, 36 | "entrypoint": null, 37 | "image": "defang.io/openai-access-gateway:latest", 38 | "networks": { 39 | "default": null 40 | }, 41 | "ports": [ 42 | { 43 | "mode": "host", 44 | "target": 80, 45 | "protocol": "tcp" 46 | } 47 | ] 48 | }, 49 | "llm": { 50 | "command": null, 51 | "entrypoint": null, 52 | "image": "llm:latest", 53 | "networks": { 54 | "default": null 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/testdata/llm/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: llm 2 | services: 3 | alt-repo: 4 | image: altrepo.com/openai-access-gateway:latest 5 | networks: 6 | default: null 7 | x-defang-llm: true 8 | gateway-with-ports: 9 | image: defang.io/openai-access-gateway:latest 10 | networks: 11 | default: null 12 | ports: 13 | - mode: ingress 14 | target: 5678 15 | published: "5678" 16 | protocol: tcp 17 | x-defang-llm: true 18 | gateway-without-ports: 19 | image: defang.io/openai-access-gateway:latest 20 | networks: 21 | default: null 22 | x-defang-llm: true 23 | llm: 24 | image: llm:latest 25 | networks: 26 | default: null 27 | x-defang-llm: true 28 | networks: 29 | default: 30 | name: llm_default 31 | -------------------------------------------------------------------------------- /src/testdata/llm/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "alt-repo": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | ! service "gateway-with-ports": ingress port without healthcheck defaults to GET / HTTP/1.1 3 | ! service "gateway-with-ports": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 4 | ! service "gateway-without-ports": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 5 | ! service "llm": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 6 | -------------------------------------------------------------------------------- /src/testdata/longname/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | aVeryLongServiceNameThatIsDefinitelyTooLongThatWillCauseAnError: 3 | image: alpine 4 | -------------------------------------------------------------------------------- /src/testdata/longname/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "aVeryLongServiceNameThatIsDefinitelyTooLongThatWillCauseAnError": { 3 | "command": null, 4 | "entrypoint": null, 5 | "image": "alpine", 6 | "networks": { 7 | "default": null 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/testdata/longname/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: longname 2 | services: 3 | aVeryLongServiceNameThatIsDefinitelyTooLongThatWillCauseAnError: 4 | image: alpine 5 | networks: 6 | default: null 7 | networks: 8 | default: 9 | name: longname_default 10 | -------------------------------------------------------------------------------- /src/testdata/longname/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "aVeryLongServiceNameThatIsDefinitelyTooLongThatWillCauseAnError": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | -------------------------------------------------------------------------------- /src/testdata/multiple/compose1.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | service1: 3 | image: example 4 | -------------------------------------------------------------------------------- /src/testdata/multiple/compose2.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | service2: 3 | image: example 4 | -------------------------------------------------------------------------------- /src/testdata/networks/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | service-invalid: 3 | image: example 4 | networks: 5 | invalid-network-name: {} 6 | service-public: 7 | image: example 8 | networks: 9 | public: {} 10 | service-public-list: 11 | image: example 12 | networks: 13 | - public 14 | service-private: 15 | image: example 16 | networks: 17 | - private 18 | service-internal: 19 | image: example 20 | networks: 21 | - internal 22 | service-default: 23 | image: example 24 | networks: 25 | - default 26 | service-multi: 27 | image: example 28 | networks: 29 | - default 30 | - internal 31 | networks: 32 | public: 33 | external: true 34 | internal: 35 | internal: true 36 | -------------------------------------------------------------------------------- /src/testdata/networks/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "service-default": { 3 | "command": null, 4 | "entrypoint": null, 5 | "image": "example", 6 | "networks": { 7 | "default": null 8 | } 9 | }, 10 | "service-internal": { 11 | "command": null, 12 | "entrypoint": null, 13 | "image": "example", 14 | "networks": { 15 | "internal": null 16 | } 17 | }, 18 | "service-invalid": { 19 | "command": null, 20 | "entrypoint": null, 21 | "image": "example", 22 | "networks": { 23 | "invalid-network-name": {} 24 | } 25 | }, 26 | "service-multi": { 27 | "command": null, 28 | "entrypoint": null, 29 | "image": "example", 30 | "networks": { 31 | "default": null, 32 | "internal": null 33 | } 34 | }, 35 | "service-private": { 36 | "command": null, 37 | "entrypoint": null, 38 | "image": "example", 39 | "networks": { 40 | "private": null 41 | } 42 | }, 43 | "service-public": { 44 | "command": null, 45 | "entrypoint": null, 46 | "image": "example", 47 | "networks": { 48 | "public": {} 49 | } 50 | }, 51 | "service-public-list": { 52 | "command": null, 53 | "entrypoint": null, 54 | "image": "example", 55 | "networks": { 56 | "public": null 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/testdata/networks/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: networks 2 | services: 3 | service-default: 4 | image: example 5 | networks: 6 | default: null 7 | service-internal: 8 | image: example 9 | networks: 10 | internal: null 11 | service-invalid: 12 | image: example 13 | networks: 14 | invalid-network-name: {} 15 | service-multi: 16 | image: example 17 | networks: 18 | default: null 19 | internal: null 20 | service-private: 21 | image: example 22 | networks: 23 | private: null 24 | service-public: 25 | image: example 26 | networks: 27 | public: {} 28 | service-public-list: 29 | image: example 30 | networks: 31 | public: null 32 | networks: 33 | default: 34 | name: networks_default 35 | internal: 36 | name: networks_internal 37 | internal: true 38 | public: 39 | name: public 40 | external: true 41 | -------------------------------------------------------------------------------- /src/testdata/networks/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "service-default": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | ! service "service-internal": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 3 | ! service "service-invalid": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 4 | ! service "service-multi": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 5 | ! service "service-private": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 6 | ! service "service-public": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 7 | ! service "service-public-list": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 8 | -------------------------------------------------------------------------------- /src/testdata/noprojname/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | echo: 3 | restart: unless-stopped 4 | image: ealen/echo-server 5 | ports: 6 | - target: 80 7 | mode: ingress 8 | healthcheck: 9 | test: ["CMD", "curl", "-f", "http://localhost/"] 10 | profiles: 11 | - donotstart 12 | 13 | secrets: 14 | dummy: 15 | external: true 16 | name: dummy 17 | -------------------------------------------------------------------------------- /src/testdata/noprojname/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/testdata/noprojname/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: noprojname 2 | services: {} 3 | networks: 4 | default: 5 | name: noprojname_default 6 | secrets: 7 | dummy: 8 | name: dummy 9 | external: true 10 | -------------------------------------------------------------------------------- /src/testdata/noprojname/compose.yaml.warnings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefangLabs/defang/d7a16134ca0af0525d6c81d6b883ecfcb82dc155/src/testdata/noprojname/compose.yaml.warnings -------------------------------------------------------------------------------- /src/testdata/platforms/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | intel2: 3 | platform: LINUX/X86_64 4 | image: busybox 5 | -------------------------------------------------------------------------------- /src/testdata/platforms/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "intel2": { 3 | "command": null, 4 | "entrypoint": null, 5 | "image": "busybox", 6 | "networks": { 7 | "default": null 8 | }, 9 | "platform": "LINUX/X86_64" 10 | } 11 | } -------------------------------------------------------------------------------- /src/testdata/platforms/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: platforms 2 | services: 3 | intel2: 4 | image: busybox 5 | networks: 6 | default: null 7 | platform: LINUX/X86_64 8 | networks: 9 | default: 10 | name: platforms_default 11 | -------------------------------------------------------------------------------- /src/testdata/platforms/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "intel2": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | -------------------------------------------------------------------------------- /src/testdata/ports/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | short: 3 | image: bogus 4 | ports: 5 | - 80 6 | short-published: 7 | image: bogus 8 | ports: 9 | - "8081:81" 10 | long: 11 | image: bogus 12 | ports: 13 | - target: 82 14 | long-published: 15 | image: bogus 16 | ports: 17 | - target: 83 18 | published: 8083 19 | short-udp: 20 | image: bogus 21 | ports: 22 | - "84/udp" 23 | short-udp-published: 24 | image: bogus 25 | ports: 26 | - "8085:85/udp" 27 | -------------------------------------------------------------------------------- /src/testdata/ports/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: ports 2 | services: 3 | long: 4 | image: bogus 5 | networks: 6 | default: null 7 | ports: 8 | - mode: ingress 9 | target: 82 10 | protocol: tcp 11 | long-published: 12 | image: bogus 13 | networks: 14 | default: null 15 | ports: 16 | - mode: ingress 17 | target: 83 18 | published: "8083" 19 | protocol: tcp 20 | short: 21 | image: bogus 22 | networks: 23 | default: null 24 | ports: 25 | - mode: ingress 26 | target: 80 27 | protocol: tcp 28 | short-published: 29 | image: bogus 30 | networks: 31 | default: null 32 | ports: 33 | - mode: ingress 34 | target: 81 35 | published: "8081" 36 | protocol: tcp 37 | short-udp: 38 | image: bogus 39 | networks: 40 | default: null 41 | ports: 42 | - mode: ingress 43 | target: 84 44 | protocol: udp 45 | short-udp-published: 46 | image: bogus 47 | networks: 48 | default: null 49 | ports: 50 | - mode: ingress 51 | target: 85 52 | published: "8085" 53 | protocol: udp 54 | networks: 55 | default: 56 | name: ports_default 57 | -------------------------------------------------------------------------------- /src/testdata/postgres/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | with-ext: 3 | image: postgres 4 | x-defang-postgres: 5 | allow-downtime: true 6 | ports: 7 | - target: 5432 8 | mode: host 9 | 10 | wrong-image: 11 | image: example 12 | x-defang-postgres: 13 | ports: 14 | - target: 5432 15 | mode: host 16 | 17 | no-ext: 18 | image: postgres 19 | ports: 20 | - target: 5432 21 | mode: host 22 | 23 | no-ports: 24 | image: postgres 25 | x-defang-postgres: 26 | 27 | no-ports-override: 28 | image: postgres 29 | x-defang-postgres: 30 | environment: 31 | - PGPORT=5433 32 | -------------------------------------------------------------------------------- /src/testdata/postgres/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "no-ext": { 3 | "command": null, 4 | "entrypoint": null, 5 | "image": "postgres", 6 | "networks": { 7 | "default": null 8 | }, 9 | "ports": [ 10 | { 11 | "mode": "host", 12 | "target": 5432, 13 | "protocol": "tcp" 14 | } 15 | ] 16 | }, 17 | "no-ports": { 18 | "command": null, 19 | "entrypoint": null, 20 | "image": "postgres", 21 | "networks": { 22 | "default": null 23 | }, 24 | "ports": [ 25 | { 26 | "mode": "host", 27 | "target": 5432, 28 | "protocol": "tcp" 29 | } 30 | ] 31 | }, 32 | "no-ports-override": { 33 | "command": null, 34 | "entrypoint": null, 35 | "environment": { 36 | "PGPORT": "5433" 37 | }, 38 | "image": "postgres", 39 | "networks": { 40 | "default": null 41 | }, 42 | "ports": [ 43 | { 44 | "mode": "host", 45 | "target": 5433, 46 | "protocol": "tcp" 47 | } 48 | ] 49 | }, 50 | "with-ext": { 51 | "command": null, 52 | "entrypoint": null, 53 | "image": "postgres", 54 | "networks": { 55 | "default": null 56 | }, 57 | "ports": [ 58 | { 59 | "mode": "host", 60 | "target": 5432, 61 | "protocol": "tcp" 62 | } 63 | ] 64 | }, 65 | "wrong-image": { 66 | "command": null, 67 | "entrypoint": null, 68 | "image": "example", 69 | "networks": { 70 | "default": null 71 | }, 72 | "ports": [ 73 | { 74 | "mode": "host", 75 | "target": 5432, 76 | "protocol": "tcp" 77 | } 78 | ] 79 | } 80 | } -------------------------------------------------------------------------------- /src/testdata/postgres/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: postgres 2 | services: 3 | no-ext: 4 | image: postgres 5 | networks: 6 | default: null 7 | ports: 8 | - mode: host 9 | target: 5432 10 | protocol: tcp 11 | no-ports: 12 | image: postgres 13 | networks: 14 | default: null 15 | x-defang-postgres: null 16 | no-ports-override: 17 | environment: 18 | PGPORT: "5433" 19 | image: postgres 20 | networks: 21 | default: null 22 | x-defang-postgres: null 23 | with-ext: 24 | image: postgres 25 | networks: 26 | default: null 27 | ports: 28 | - mode: host 29 | target: 5432 30 | protocol: tcp 31 | x-defang-postgres: 32 | allow-downtime: true 33 | wrong-image: 34 | image: example 35 | networks: 36 | default: null 37 | ports: 38 | - mode: host 39 | target: 5432 40 | protocol: tcp 41 | x-defang-postgres: null 42 | networks: 43 | default: 44 | name: postgres_default 45 | -------------------------------------------------------------------------------- /src/testdata/postgres/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "no-ext": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | ! service "no-ext": stateful service will lose data on restart; use a managed service instead 3 | ! service "no-ports": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 4 | ! service "no-ports-override": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 5 | ! service "with-ext": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 6 | ! service "wrong-image": managed Postgres service should use a postgres image 7 | ! service "wrong-image": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 8 | -------------------------------------------------------------------------------- /src/testdata/profiles/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | always: 3 | image: alpine 4 | 5 | never: 6 | image: alpine 7 | profiles: 8 | - never 9 | 10 | defangonly: 11 | image: alpine 12 | profiles: 13 | - defang 14 | -------------------------------------------------------------------------------- /src/testdata/profiles/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "always": { 3 | "command": null, 4 | "entrypoint": null, 5 | "image": "alpine", 6 | "networks": { 7 | "default": null 8 | } 9 | }, 10 | "defangonly": { 11 | "profiles": [ 12 | "defang" 13 | ], 14 | "command": null, 15 | "entrypoint": null, 16 | "image": "alpine", 17 | "networks": { 18 | "default": null 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/testdata/profiles/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: profiles 2 | services: 3 | always: 4 | image: alpine 5 | networks: 6 | default: null 7 | defangonly: 8 | profiles: 9 | - defang 10 | image: alpine 11 | networks: 12 | default: null 13 | networks: 14 | default: 15 | name: profiles_default 16 | -------------------------------------------------------------------------------- /src/testdata/profiles/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "always": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | ! service "defangonly": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 3 | -------------------------------------------------------------------------------- /src/testdata/provider/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | chat: 3 | image: my-chat-app 4 | # image: curlimages/curl 5 | # command: 6 | # - "sh" 7 | # - "-c" 8 | # - "curl -sf $${AI_RUNNER_URL}chat/completions -H 'Content-Type: application/json' -d '{\"model\":\"anthropic.claude-3-5-sonnet-20241022-v2:0\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello, world!\"}]}'; sleep 5" 9 | depends_on: 10 | - ai_runner 11 | 12 | ai_runner: 13 | provider: 14 | type: model 15 | options: 16 | model: ai/smollm2 17 | # x-defang-llm: true 18 | -------------------------------------------------------------------------------- /src/testdata/provider/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "ai_runner": { 3 | "command": null, 4 | "entrypoint": null, 5 | "environment": { 6 | "OPENAI_API_KEY": "" 7 | }, 8 | "image": "defangio/openai-access-gateway", 9 | "networks": { 10 | "model_provider_private": null 11 | }, 12 | "ports": [ 13 | { 14 | "mode": "host", 15 | "target": 80, 16 | "protocol": "tcp" 17 | } 18 | ] 19 | }, 20 | "chat": { 21 | "command": null, 22 | "depends_on": { 23 | "ai_runner": { 24 | "condition": "service_started", 25 | "required": true 26 | } 27 | }, 28 | "entrypoint": null, 29 | "environment": { 30 | "AI_RUNNER_MODEL": "ai/smollm2", 31 | "AI_RUNNER_URL": "http://ai-runner/api/v1/" 32 | }, 33 | "image": "my-chat-app", 34 | "networks": { 35 | "default": null, 36 | "model_provider_private": null 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/testdata/provider/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: provider 2 | services: 3 | ai_runner: 4 | provider: 5 | type: model 6 | options: 7 | model: 8 | - ai/smollm2 9 | networks: 10 | default: null 11 | chat: 12 | depends_on: 13 | ai_runner: 14 | condition: service_started 15 | required: true 16 | image: my-chat-app 17 | networks: 18 | default: null 19 | networks: 20 | default: 21 | name: provider_default 22 | -------------------------------------------------------------------------------- /src/testdata/provider/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "ai_runner": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | ! service "chat": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 3 | ! service "chat": service name was adjusted: environment variable "AI_RUNNER_URL" assigned value "http://mock-ai-runner/api/v1/" 4 | -------------------------------------------------------------------------------- /src/testdata/redis/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | with-ext: 3 | image: redis 4 | x-defang-redis: 5 | allow-downtime: true 6 | ports: 7 | - target: 6379 8 | mode: host 9 | wrong-image: 10 | image: example 11 | x-defang-redis: 12 | ports: 13 | - target: 6379 14 | mode: host 15 | 16 | no-ext: 17 | image: redis 18 | ports: 19 | - target: 6379 20 | mode: host 21 | 22 | no-ports: 23 | image: redis 24 | x-defang-redis: 25 | 26 | no-ports-override: 27 | image: redis 28 | x-defang-redis: 29 | command: ["--port", "6380"] 30 | -------------------------------------------------------------------------------- /src/testdata/redis/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "no-ext": { 3 | "command": null, 4 | "entrypoint": null, 5 | "image": "redis", 6 | "networks": { 7 | "default": null 8 | }, 9 | "ports": [ 10 | { 11 | "mode": "host", 12 | "target": 6379, 13 | "protocol": "tcp" 14 | } 15 | ] 16 | }, 17 | "no-ports": { 18 | "command": null, 19 | "entrypoint": null, 20 | "image": "redis", 21 | "networks": { 22 | "default": null 23 | }, 24 | "ports": [ 25 | { 26 | "mode": "host", 27 | "target": 6379, 28 | "protocol": "tcp" 29 | } 30 | ] 31 | }, 32 | "no-ports-override": { 33 | "command": [ 34 | "--port", 35 | "6380" 36 | ], 37 | "entrypoint": null, 38 | "image": "redis", 39 | "networks": { 40 | "default": null 41 | }, 42 | "ports": [ 43 | { 44 | "mode": "host", 45 | "target": 6380, 46 | "protocol": "tcp" 47 | } 48 | ] 49 | }, 50 | "with-ext": { 51 | "command": null, 52 | "entrypoint": null, 53 | "image": "redis", 54 | "networks": { 55 | "default": null 56 | }, 57 | "ports": [ 58 | { 59 | "mode": "host", 60 | "target": 6379, 61 | "protocol": "tcp" 62 | } 63 | ] 64 | }, 65 | "wrong-image": { 66 | "command": null, 67 | "entrypoint": null, 68 | "image": "example", 69 | "networks": { 70 | "default": null 71 | }, 72 | "ports": [ 73 | { 74 | "mode": "host", 75 | "target": 6379, 76 | "protocol": "tcp" 77 | } 78 | ] 79 | } 80 | } -------------------------------------------------------------------------------- /src/testdata/redis/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: redis 2 | services: 3 | no-ext: 4 | image: redis 5 | networks: 6 | default: null 7 | ports: 8 | - mode: host 9 | target: 6379 10 | protocol: tcp 11 | no-ports: 12 | image: redis 13 | networks: 14 | default: null 15 | x-defang-redis: null 16 | no-ports-override: 17 | command: 18 | - --port 19 | - "6380" 20 | image: redis 21 | networks: 22 | default: null 23 | x-defang-redis: null 24 | with-ext: 25 | image: redis 26 | networks: 27 | default: null 28 | ports: 29 | - mode: host 30 | target: 6379 31 | protocol: tcp 32 | x-defang-redis: 33 | allow-downtime: true 34 | wrong-image: 35 | image: example 36 | networks: 37 | default: null 38 | ports: 39 | - mode: host 40 | target: 6379 41 | protocol: tcp 42 | x-defang-redis: null 43 | networks: 44 | default: 45 | name: redis_default 46 | -------------------------------------------------------------------------------- /src/testdata/redis/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "no-ext": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | ! service "no-ext": stateful service will lose data on restart; use a managed service instead 3 | ! service "no-ports": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 4 | ! service "no-ports-override": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 5 | ! service "with-ext": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 6 | ! service "wrong-image": managed Redis service should use a redis image 7 | ! service "wrong-image": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 8 | -------------------------------------------------------------------------------- /src/testdata/sanity/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | nginx: 3 | restart: unless-stopped 4 | image: nginx 5 | environment: 6 | - dummy 7 | ports: 8 | - target: 80 9 | mode: ingress 10 | deploy: 11 | resources: 12 | reservations: 13 | memory: 256M 14 | -------------------------------------------------------------------------------- /src/testdata/sanity/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "nginx": { 3 | "command": null, 4 | "deploy": { 5 | "resources": { 6 | "reservations": { 7 | "memory": "268435456" 8 | } 9 | }, 10 | "placement": {} 11 | }, 12 | "entrypoint": null, 13 | "environment": { 14 | "dummy": null 15 | }, 16 | "image": "nginx", 17 | "networks": { 18 | "default": null 19 | }, 20 | "ports": [ 21 | { 22 | "mode": "ingress", 23 | "target": 80, 24 | "protocol": "tcp", 25 | "app_protocol": "http" 26 | } 27 | ], 28 | "restart": "unless-stopped" 29 | } 30 | } -------------------------------------------------------------------------------- /src/testdata/sanity/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: sanity 2 | services: 3 | nginx: 4 | deploy: 5 | resources: 6 | reservations: 7 | memory: "268435456" 8 | environment: 9 | dummy: null 10 | image: nginx 11 | networks: 12 | default: null 13 | ports: 14 | - mode: ingress 15 | target: 80 16 | protocol: tcp 17 | restart: unless-stopped 18 | networks: 19 | default: 20 | name: sanity_default 21 | -------------------------------------------------------------------------------- /src/testdata/sanity/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "nginx": ingress port without healthcheck defaults to GET / HTTP/1.1 2 | -------------------------------------------------------------------------------- /src/testdata/secretname/Dockerfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefangLabs/defang/d7a16134ca0af0525d6c81d6b883ecfcb82dc155/src/testdata/secretname/Dockerfile -------------------------------------------------------------------------------- /src/testdata/secretname/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | restart: unless-stopped 4 | build: 5 | context: . 6 | secrets: 7 | - dummy 8 | secrets: 9 | dummy: 10 | external: true 11 | # name: dummyx ## this causes a validation error in compose-go 12 | -------------------------------------------------------------------------------- /src/testdata/secretname/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "build": { 4 | "context": ".", 5 | "dockerfile": "Dockerfile" 6 | }, 7 | "command": null, 8 | "entrypoint": null, 9 | "environment": { 10 | "dummy": null 11 | }, 12 | "networks": { 13 | "default": null 14 | }, 15 | "restart": "unless-stopped" 16 | } 17 | } -------------------------------------------------------------------------------- /src/testdata/secretname/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: secretname 2 | services: 3 | app: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | networks: 8 | default: null 9 | restart: unless-stopped 10 | secrets: 11 | - source: dummy 12 | target: /run/secrets/dummy 13 | networks: 14 | default: 15 | name: secretname_default 16 | secrets: 17 | dummy: 18 | name: dummy 19 | external: true 20 | -------------------------------------------------------------------------------- /src/testdata/secretname/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "app": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | ! service "app": secrets will be exposed as environment variables, not files (use 'environment' instead) 3 | * Packaging the project files for app at . 4 | -------------------------------------------------------------------------------- /src/testdata/static-files/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | x: 3 | image: blah 4 | x-defang-static-files: ./folder 5 | y: 6 | image: blah 7 | x-unsupported: asdf 8 | x-defang-static-files: 9 | folder: ./folder 10 | redirects: true 11 | - www.example.com 12 | -------------------------------------------------------------------------------- /src/testdata/static-files/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "x": { 3 | "command": null, 4 | "entrypoint": null, 5 | "image": "blah", 6 | "networks": { 7 | "default": null 8 | } 9 | }, 10 | "y": { 11 | "command": null, 12 | "entrypoint": null, 13 | "image": "blah", 14 | "networks": { 15 | "default": null 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/testdata/static-files/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: static-files 2 | services: 3 | x: 4 | image: blah 5 | networks: 6 | default: null 7 | x-defang-static-files: ./folder 8 | "y": 9 | image: blah 10 | networks: 11 | default: null 12 | x-defang-static-files: 13 | folder: ./folder 14 | redirects: true - www.example.com 15 | x-unsupported: asdf 16 | networks: 17 | default: 18 | name: static-files_default 19 | -------------------------------------------------------------------------------- /src/testdata/static-files/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "y": unsupported compose extension: "x-unsupported" 2 | -------------------------------------------------------------------------------- /src/testdata/testproj/.dockerignore: -------------------------------------------------------------------------------- 1 | # Default .dockerignore file for Defang 2 | **/__pycache__ 3 | **/.direnv 4 | **/.DS_Store 5 | **/.envrc 6 | **/.git 7 | **/.github 8 | **/.idea 9 | **/.vscode 10 | **/compose.yaml 11 | **/compose.yml 12 | **/defang.exe 13 | **/docker-compose.yaml 14 | **/docker-compose.yml 15 | **/node_modules 16 | **/Thumbs.db 17 | defang 18 | 19 | # Test to ensure Dockerfile is always included, even when excluded by .dockerignore 20 | Dockerfile 21 | .dockerignore 22 | 23 | **/compose.yaml.golden 24 | **/compose.yaml.warnings 25 | **/compose.yaml.convert 26 | **/compose.yaml.fixup 27 | -------------------------------------------------------------------------------- /src/testdata/testproj/.env: -------------------------------------------------------------------------------- 1 | DOTENV=enabled 2 | -------------------------------------------------------------------------------- /src/testdata/testproj/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest AS testproj 2 | ARG DNS 3 | ENV DNS=$DNS 4 | CMD ["sh", "-c", "while true; do nslookup ${DNS} ; sleep 10 ; done"] 5 | -------------------------------------------------------------------------------- /src/testdata/testproj/compose.yaml: -------------------------------------------------------------------------------- 1 | name: tests 2 | services: 3 | dfnx: 4 | restart: unless-stopped 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | target: testproj 9 | args: 10 | DNS: dfnx 11 | env_file: 12 | - fileName.env 13 | deploy: 14 | resources: 15 | limits: 16 | cpus: '2.0' 17 | memory: 512M 18 | reservations: 19 | cpus: '0.25' 20 | memory: 256M 21 | ports: 22 | - target: 80 23 | mode: ingress 24 | - target: 1234 25 | # mode: ingress 26 | - target: 4567 27 | protocol: udp 28 | mode: ingress 29 | secrets: 30 | - dummy 31 | healthcheck: 32 | test: ["CMD", "curl", "-f", "http://localhost/"] 33 | environment: 34 | - DOTENV 35 | - DOT_ENV_INTERPOLATION=${DOTENV} 36 | # disable: true 37 | 38 | # dfnx: 39 | # build: 40 | # context: . 41 | # dockerfile: Dockerfile.dfn 42 | # ports: 43 | # - 80 44 | 45 | echo: 46 | image: ealen/echo-server 47 | ports: 48 | - target: 80 49 | mode: ingress 50 | healthcheck: 51 | test: ["CMD", "curl", "-f", "http://localhost/"] 52 | # domainname: echotest.gnafed.click 53 | profiles: 54 | - donotstart 55 | x-defang-dns-role: arn:aws:iam::123456789012:role/ecs-service-role 56 | 57 | secrets: 58 | dummy: 59 | external: true 60 | name: dummy 61 | 62 | x-unsupported: unsupported 63 | -------------------------------------------------------------------------------- /src/testdata/testproj/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "dfnx": { 3 | "build": { 4 | "context": ".", 5 | "dockerfile": "Dockerfile", 6 | "args": { 7 | "DNS": "dfnx" 8 | }, 9 | "target": "testproj" 10 | }, 11 | "command": null, 12 | "deploy": { 13 | "resources": { 14 | "limits": { 15 | "cpus": 2, 16 | "memory": "536870912" 17 | }, 18 | "reservations": { 19 | "cpus": 0.25, 20 | "memory": "268435456" 21 | } 22 | }, 23 | "placement": {} 24 | }, 25 | "entrypoint": null, 26 | "environment": { 27 | "DOTENV": "enabled", 28 | "DOT_ENV_INTERPOLATION": "enabled", 29 | "FOO": "bar", 30 | "dummy": null 31 | }, 32 | "healthcheck": { 33 | "test": [ 34 | "CMD", 35 | "curl", 36 | "-f", 37 | "http://localhost/" 38 | ] 39 | }, 40 | "networks": { 41 | "default": null 42 | }, 43 | "ports": [ 44 | { 45 | "mode": "ingress", 46 | "target": 80, 47 | "protocol": "tcp", 48 | "app_protocol": "http" 49 | }, 50 | { 51 | "mode": "ingress", 52 | "target": 1234, 53 | "protocol": "tcp", 54 | "app_protocol": "http" 55 | }, 56 | { 57 | "mode": "host", 58 | "target": 4567, 59 | "protocol": "udp" 60 | } 61 | ], 62 | "restart": "unless-stopped" 63 | } 64 | } -------------------------------------------------------------------------------- /src/testdata/testproj/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: tests 2 | services: 3 | dfnx: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | args: 8 | DNS: dfnx 9 | target: testproj 10 | deploy: 11 | resources: 12 | limits: 13 | cpus: 2 14 | memory: "536870912" 15 | reservations: 16 | cpus: 0.25 17 | memory: "268435456" 18 | environment: 19 | DOT_ENV_INTERPOLATION: enabled 20 | DOTENV: enabled 21 | FOO: bar 22 | healthcheck: 23 | test: 24 | - CMD 25 | - curl 26 | - -f 27 | - http://localhost/ 28 | networks: 29 | default: null 30 | ports: 31 | - mode: ingress 32 | target: 80 33 | protocol: tcp 34 | - mode: ingress 35 | target: 1234 36 | protocol: tcp 37 | - mode: ingress 38 | target: 4567 39 | protocol: udp 40 | restart: unless-stopped 41 | secrets: 42 | - source: dummy 43 | target: /run/secrets/dummy 44 | networks: 45 | default: 46 | name: tests_default 47 | secrets: 48 | dummy: 49 | name: dummy 50 | external: true 51 | x-unsupported: unsupported 52 | -------------------------------------------------------------------------------- /src/testdata/testproj/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! port 4567: UDP ports default to 'host' mode (add 'mode: host' to silence) 2 | ! service "dfnx": secrets will be exposed as environment variables, not files (use 'environment' instead) 3 | ! service "dfnx": service name was adjusted: build argument "DNS" assigned value "mock-dfnx" 4 | * Packaging the project files for dfnx at . 5 | -------------------------------------------------------------------------------- /src/testdata/testproj/fileName.env: -------------------------------------------------------------------------------- 1 | FOO=bar 2 | -------------------------------------------------------------------------------- /src/testdata/toomany/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | service1: 3 | image: example 4 | -------------------------------------------------------------------------------- /src/testdata/toomany/compose.yaml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "service1": { 3 | "command": null, 4 | "entrypoint": null, 5 | "image": "example", 6 | "networks": { 7 | "default": null 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/testdata/toomany/compose.yaml.golden: -------------------------------------------------------------------------------- 1 | name: toomany 2 | services: 3 | service1: 4 | image: example 5 | networks: 6 | default: null 7 | networks: 8 | default: 9 | name: toomany_default 10 | -------------------------------------------------------------------------------- /src/testdata/toomany/compose.yaml.warnings: -------------------------------------------------------------------------------- 1 | ! service "service1": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | -------------------------------------------------------------------------------- /src/testdata/toomany/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | service1: 3 | image: example 4 | -------------------------------------------------------------------------------- /src/testdata/toomany/docker-compose.yml.fixup: -------------------------------------------------------------------------------- 1 | { 2 | "service1": { 3 | "command": null, 4 | "entrypoint": null, 5 | "image": "example", 6 | "networks": { 7 | "default": null 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/testdata/toomany/docker-compose.yml.golden: -------------------------------------------------------------------------------- 1 | name: toomany 2 | services: 3 | service1: 4 | image: example 5 | networks: 6 | default: null 7 | networks: 8 | default: 9 | name: toomany_default 10 | -------------------------------------------------------------------------------- /src/testdata/toomany/docker-compose.yml.warnings: -------------------------------------------------------------------------------- 1 | ! service "service1": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors 2 | --------------------------------------------------------------------------------