From 4bbdb662996c51ad6c911e1faae6029b3030fcea Mon Sep 17 00:00:00 2001 From: RogerCecilpot Date: Sun, 25 Oct 2026 12:44:13 +0000 Subject: [PATCH 1/3] feat: vendor pruned API specs and generate clients hack/specprune turns the platform's private OpenAPI specs into public copies under internal/api/specs, driven by a reviewed allowlist: only the operations the tier 1 and 2 commands use and the components they reference are kept; servers, security requirements and every x- extension are removed; game-server fields are dropped; descriptions that name internal services are rewritten. It refuses to write anything while a description matches a deny pattern or a property name looks internal and has not been reviewed, so #237's automation can run it unattended. hack/vendor-specs.sh (make vendor-specs) fetches the allowlisted files at a pinned ref; the vendored specs record their source commit. Clients are generated with oapi-codegen v2.5.1, the platform's version, into internal/api/gen/ (make generate). CI fails if generated code is out of date. Refs devlyft/devlyft#236 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/checks.yml | 5 + Makefile | 9 +- go.mod | 33 + go.sum | 192 +- hack/specprune/main.go | 517 +++++ hack/specprune/main_test.go | 185 ++ hack/vendor-specs.sh | 31 + internal/api/doc.go | 3 - internal/api/gen/deploys/cfg.yaml | 6 + internal/api/gen/deploys/deploys.gen.go | 2540 +++++++++++++++++++++++ internal/api/gen/deploys/generate.go | 5 + internal/api/gen/logs/cfg.yaml | 6 + internal/api/gen/logs/generate.go | 5 + internal/api/gen/logs/logs.gen.go | 369 ++++ internal/api/gen/metrics/cfg.yaml | 6 + internal/api/gen/metrics/generate.go | 5 + internal/api/gen/metrics/metrics.gen.go | 625 ++++++ internal/api/gen/org/cfg.yaml | 6 + internal/api/gen/org/generate.go | 5 + internal/api/gen/org/org.gen.go | 2064 ++++++++++++++++++ internal/api/specs/allowlist.yaml | 112 + internal/api/specs/deploys.yaml | 947 +++++++++ internal/api/specs/logs.yaml | 101 + internal/api/specs/metrics.yaml | 169 ++ internal/api/specs/org.yaml | 660 ++++++ 25 files changed, 8600 insertions(+), 6 deletions(-) create mode 100644 hack/specprune/main.go create mode 100644 hack/specprune/main_test.go create mode 100755 hack/vendor-specs.sh delete mode 100644 internal/api/doc.go create mode 100644 internal/api/gen/deploys/cfg.yaml create mode 100644 internal/api/gen/deploys/deploys.gen.go create mode 100644 internal/api/gen/deploys/generate.go create mode 100644 internal/api/gen/logs/cfg.yaml create mode 100644 internal/api/gen/logs/generate.go create mode 100644 internal/api/gen/logs/logs.gen.go create mode 100644 internal/api/gen/metrics/cfg.yaml create mode 100644 internal/api/gen/metrics/generate.go create mode 100644 internal/api/gen/metrics/metrics.gen.go create mode 100644 internal/api/gen/org/cfg.yaml create mode 100644 internal/api/gen/org/generate.go create mode 100644 internal/api/gen/org/org.gen.go create mode 100644 internal/api/specs/allowlist.yaml create mode 100644 internal/api/specs/deploys.yaml create mode 100644 internal/api/specs/logs.yaml create mode 100644 internal/api/specs/metrics.yaml create mode 100644 internal/api/specs/org.yaml diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 54b3df0..2dbb608 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -24,6 +24,11 @@ jobs: run: | go mod tidy git diff --exit-code -- go.mod go.sum + - name: generated code is up to date + run: | + make generate + git diff --exit-code + test -z "$(git status --porcelain)" test: name: test (${{ matrix.os }}) diff --git a/Makefile b/Makefile index 198e754..96728f3 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) LDFLAGS := -s -w -X github.com/devlyft/cli/internal/version.Version=$(VERSION) .DEFAULT_GOAL := build -.PHONY: build test cover lint fmt vuln tidy check snapshot clean help +.PHONY: build test cover lint fmt vuln tidy generate vendor-specs check snapshot clean help build: ## Build the devlyft binary into bin/ go build -trimpath -ldflags '$(LDFLAGS)' -o $(BIN) ./cmd/devlyft @@ -32,6 +32,13 @@ vuln: ## Check dependencies for known vulnerabilities tidy: ## Tidy go.mod and go.sum go mod tidy +generate: ## Regenerate API clients and mocks from internal/api/specs + go generate ./internal/api/... + +vendor-specs: ## Re-vendor pruned specs from the platform repo (needs access; REF=) + hack/vendor-specs.sh $(REF) + $(MAKE) generate + check: lint test vuln ## Run everything CI runs snapshot: ## Build unsigned release archives for every platform into dist/ diff --git a/go.mod b/go.mod index 81d8b3d..5eb697f 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,49 @@ module github.com/devlyft/cli go 1.26.0 require ( + github.com/getkin/kin-openapi v0.133.0 + github.com/google/uuid v1.6.0 github.com/itchyny/gojq v0.12.19 + github.com/oapi-codegen/runtime v1.6.0 github.com/spf13/cobra v1.10.2 + go.uber.org/mock v0.6.0 + go.yaml.in/yaml/v3 v3.0.5 golang.org/x/term v0.47.0 sigs.k8s.io/yaml v1.6.0 ) require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/itchyny/timefmt-go v0.1.8 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oapi-codegen/oapi-codegen/v2 v2.5.1 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/speakeasy-api/jsonpath v0.6.0 // indirect + github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/spf13/pflag v1.0.9 // indirect + github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.49.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/tools v0.39.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +tool ( + github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen + go.uber.org/mock/mockgen ) diff --git a/go.sum b/go.sum index d7f84b4..d0eaa58 100644 --- a/go.sum +++ b/go.sum @@ -1,26 +1,214 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM= +github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/itchyny/gojq v0.12.19 h1:ttXA0XCLEMoaLOz5lSeFOZ6u6Q3QxmG46vfgI4O0DEs= github.com/itchyny/gojq v0.12.19/go.mod h1:5galtVPDywX8SPSOrqjGxkBeDhSxEW1gSxoy7tn1iZY= github.com/itchyny/timefmt-go v0.1.8 h1:1YEo1JvfXeAHKdjelbYr/uCuhkybaHCeTkH8Bo791OI= github.com/itchyny/timefmt-go v0.1.8/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= +github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= +github.com/oapi-codegen/oapi-codegen/v2 v2.5.1 h1:5vHNY1uuPBRBWqB2Dp0G7YB03phxLQZupZTIZaeorjc= +github.com/oapi-codegen/oapi-codegen/v2 v2.5.1/go.mod h1:ro0npU1BWkcGpCgGD9QwPp44l5OIZ94tB3eabnT7DjQ= +github.com/oapi-codegen/runtime v1.6.0 h1:7Xx+GlueD6nRuyKoCPzL434Jfi3BetbiJOrzCHp/VPU= +github.com/oapi-codegen/runtime v1.6.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= +github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= +github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= +github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= +github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/hack/specprune/main.go b/hack/specprune/main.go new file mode 100644 index 0000000..af546cb --- /dev/null +++ b/hack/specprune/main.go @@ -0,0 +1,517 @@ +// Command specprune turns the platform's private OpenAPI specs into the +// pruned public copies vendored under internal/api/specs. +// +// This repository is public, so nothing reaches it unless the allowlist +// names it. For each service the tool keeps only the allowlisted operations +// and the components they reference, removes every x- extension, security +// requirement and server, drops the fields the allowlist names, applies +// description overrides, and then refuses to write anything if a remaining +// description matches a deny pattern or a property name looks internal and +// has not been reviewed. +// +// Usage: +// +// specprune -allowlist internal/api/specs/allowlist.yaml -src -out internal/api/specs +// +// holds the source specs at the paths the allowlist names, checked out +// at the allowlist's source ref (see hack/vendor-specs.sh). +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "os" + "path/filepath" + "regexp" + "slices" + "strings" + + "go.yaml.in/yaml/v3" +) + +// Allowlist is the reviewed description of what may be published. +type Allowlist struct { + Source struct { + Repo string `yaml:"repo"` + Ref string `yaml:"ref"` + } `yaml:"source"` + // DenyPatterns are regular expressions no published description may + // match. + DenyPatterns []string `yaml:"deny_patterns"` + // SuspectFields are regular expressions for property names that need a + // human decision: each match must be dropped or listed under reviewed. + SuspectFields []string `yaml:"suspect_fields"` + Services map[string]Service `yaml:"services"` +} + +// Service is one source spec. +type Service struct { + Path string `yaml:"path"` + Operations []string `yaml:"operations"` + // DropFields removes properties: "schemas/." or + // "requestBodies/.". + DropFields []string `yaml:"drop_fields"` + // Reviewed lists suspect properties a human decided to publish, in the + // same form as DropFields. + Reviewed []string `yaml:"reviewed"` + // Descriptions replaces descriptions by JSON pointer; an empty value + // removes the description. + Descriptions map[string]string `yaml:"descriptions"` +} + +var httpMethods = []string{"get", "put", "post", "delete", "options", "head", "patch", "trace"} + +func main() { + allowPath := flag.String("allowlist", "internal/api/specs/allowlist.yaml", "allowlist file") + src := flag.String("src", "", "directory holding the source specs") + out := flag.String("out", "internal/api/specs", "output directory") + flag.Parse() + + if err := run(*allowPath, *src, *out); err != nil { + fmt.Fprintln(os.Stderr, "specprune:", err) + os.Exit(1) + } +} + +func run(allowPath, src, out string) error { + if src == "" { + return errors.New("-src is required") + } + b, err := os.ReadFile(allowPath) //nolint:gosec // path from a flag + if err != nil { + return err + } + var al Allowlist + dec := yaml.NewDecoder(bytes.NewReader(b)) + dec.KnownFields(true) + if err := dec.Decode(&al); err != nil { + return fmt.Errorf("%s: %w", allowPath, err) + } + deny, err := compileAll(al.DenyPatterns) + if err != nil { + return err + } + suspect, err := compileAll(al.SuspectFields) + if err != nil { + return err + } + + names := make([]string, 0, len(al.Services)) + for name := range al.Services { + names = append(names, name) + } + slices.Sort(names) + + var problems []string + results := map[string][]byte{} + for _, name := range names { + svc := al.Services[name] + raw, err := os.ReadFile(filepath.Join(src, svc.Path)) //nolint:gosec // paths come from the reviewed allowlist + if err != nil { + return err + } + pruned, probs, err := prune(raw, &svc, deny, suspect) + if err != nil { + return fmt.Errorf("%s: %w", name, err) + } + for _, p := range probs { + problems = append(problems, name+": "+p) + } + header := fmt.Sprintf("# Code generated by hack/specprune from %s@%s:%s. DO NOT EDIT.\n"+ + "# Pruned to the operations in allowlist.yaml; regenerate with `make vendor-specs`.\n", + al.Source.Repo, al.Source.Ref, svc.Path) + results[name] = append([]byte(header), pruned...) + } + if len(problems) > 0 { + return fmt.Errorf("refusing to publish:\n %s", strings.Join(problems, "\n ")) + } + for _, name := range names { + //nolint:gosec // committed source files are world-readable + if err := os.WriteFile(filepath.Join(out, name+".yaml"), results[name], 0o644); err != nil { + return err + } + } + return nil +} + +func compileAll(patterns []string) ([]*regexp.Regexp, error) { + res := make([]*regexp.Regexp, 0, len(patterns)) + for _, p := range patterns { + re, err := regexp.Compile(p) + if err != nil { + return nil, fmt.Errorf("pattern %q: %w", p, err) + } + res = append(res, re) + } + return res, nil +} + +// prune returns the pruned spec and any reasons it must not be published. +func prune(raw []byte, svc *Service, deny, suspect []*regexp.Regexp) ([]byte, []string, error) { + var doc yaml.Node + if err := yaml.Unmarshal(raw, &doc); err != nil { + return nil, nil, err + } + root := doc.Content[0] + + // Top level: keep only what a client needs. + keepKeys(root, "openapi", "info", "paths", "components") + if info := get(root, "info"); info != nil { + keepKeys(info, "title", "version") + } + + if err := pruneOperations(get(root, "paths"), svc.Operations); err != nil { + return nil, nil, err + } + stripExtensions(root) + if err := dropFields(root, svc.DropFields); err != nil { + return nil, nil, err + } + pruneComponents(root) + if err := overrideDescriptions(root, svc.Descriptions); err != nil { + return nil, nil, err + } + + var problems []string + walkDescriptions(root, "", func(ptr, text string) { + for _, re := range deny { + if re.MatchString(text) { + problems = append(problems, fmt.Sprintf("description at %s matches deny pattern %q", ptr, re)) + } + } + }) + for _, f := range propertyNames(root) { + if slices.Contains(svc.Reviewed, f) { + continue + } + for _, re := range suspect { + if re.MatchString(f[strings.LastIndex(f, ".")+1:]) { + problems = append(problems, fmt.Sprintf("property %s looks internal: drop it or list it under reviewed", f)) + } + } + } + + var buf bytes.Buffer + enc := yaml.NewEncoder(&buf) + enc.SetIndent(2) + if err := enc.Encode(&doc); err != nil { + return nil, nil, err + } + return buf.Bytes(), problems, enc.Close() +} + +// pruneOperations keeps only allowlisted operations and removes path items +// left without any. +func pruneOperations(paths *yaml.Node, ops []string) error { + if paths == nil { + return errors.New("no paths") + } + found := map[string]bool{} + var kept []*yaml.Node + for i := 0; i < len(paths.Content); i += 2 { + item := paths.Content[i+1] + var itemKept []*yaml.Node + hasOp := false + for j := 0; j < len(item.Content); j += 2 { + k, v := item.Content[j], item.Content[j+1] + if !slices.Contains(httpMethods, k.Value) { + itemKept = append(itemKept, k, v) + continue + } + id := scalar(get(v, "operationId")) + if !slices.Contains(ops, id) { + continue + } + found[id] = true + hasOp = true + deleteKey(v, "security") + itemKept = append(itemKept, k, v) + } + if hasOp { + item.Content = itemKept + kept = append(kept, paths.Content[i], item) + } + } + paths.Content = kept + for _, id := range ops { + if !found[id] { + return fmt.Errorf("allowlisted operation %s not found", id) + } + } + return nil +} + +// stripExtensions removes every x- key. A schema whose Go type came from +// x-go-type and that has no type of its own becomes a string, which is how +// those types (resource quantities) serialise. +func stripExtensions(n *yaml.Node) { + switch n.Kind { //nolint:exhaustive // scalars and aliases carry no keys + case yaml.MappingNode: + hadGoType := false + var kept []*yaml.Node + for i := 0; i < len(n.Content); i += 2 { + k := n.Content[i] + if strings.HasPrefix(k.Value, "x-") { + if k.Value == "x-go-type" { + hadGoType = true + } + continue + } + kept = append(kept, k, n.Content[i+1]) + } + n.Content = kept + if hadGoType && get(n, "type") == nil && get(n, "$ref") == nil { + n.Content = append([]*yaml.Node{str("type"), str("string")}, n.Content...) + } + for _, c := range n.Content { + stripExtensions(c) + } + case yaml.SequenceNode, yaml.DocumentNode: + for _, c := range n.Content { + stripExtensions(c) + } + } +} + +// dropFields removes properties named "schemas/." or +// "requestBodies/.", and their entries in required. +func dropFields(root *yaml.Node, fields []string) error { + for _, f := range fields { + obj, err := objectSchema(root, f[:strings.LastIndex(f, ".")]) + if err != nil { + return fmt.Errorf("drop_fields %s: %w", f, err) + } + field := f[strings.LastIndex(f, ".")+1:] + props := get(obj, "properties") + if props == nil || get(props, field) == nil { + return fmt.Errorf("drop_fields %s: no such property", f) + } + deleteKey(props, field) + if req := get(obj, "required"); req != nil { + var kept []*yaml.Node + for _, r := range req.Content { + if r.Value != field { + kept = append(kept, r) + } + } + req.Content = kept + } + } + return nil +} + +// objectSchema resolves "schemas/" or "requestBodies/" to the +// object schema holding properties. +func objectSchema(root *yaml.Node, ref string) (*yaml.Node, error) { + kind, name, ok := strings.Cut(ref, "/") + if !ok { + return nil, fmt.Errorf("want schemas/ or requestBodies/, got %q", ref) + } + n := get(get(get(root, "components"), kind), name) + if n == nil { + return nil, errors.New("not found") + } + if kind == "requestBodies" { + n = get(get(get(n, "content"), "application/json"), "schema") + } + if n == nil { + return nil, errors.New("no JSON schema") + } + return n, nil +} + +// pruneComponents keeps only components reachable from the paths. +func pruneComponents(root *yaml.Node) { + comps := get(root, "components") + if comps == nil { + return + } + reachable := map[string]bool{} + var queue []string + collectRefs(get(root, "paths"), &queue) + for len(queue) > 0 { + ref := queue[0] + queue = queue[1:] + if reachable[ref] { + continue + } + reachable[ref] = true + kind, name, _ := strings.Cut(strings.TrimPrefix(ref, "#/components/"), "/") + if n := get(get(comps, kind), name); n != nil { + collectRefs(n, &queue) + } + } + var keptKinds []*yaml.Node + for i := 0; i < len(comps.Content); i += 2 { + kind, items := comps.Content[i], comps.Content[i+1] + var kept []*yaml.Node + for j := 0; j < len(items.Content); j += 2 { + if reachable["#/components/"+kind.Value+"/"+items.Content[j].Value] { + kept = append(kept, items.Content[j], items.Content[j+1]) + } + } + items.Content = kept + if len(kept) > 0 { + keptKinds = append(keptKinds, kind, items) + } + } + comps.Content = keptKinds +} + +func collectRefs(n *yaml.Node, out *[]string) { + if n == nil { + return + } + if n.Kind == yaml.MappingNode { + for i := 0; i < len(n.Content); i += 2 { + if n.Content[i].Value == "$ref" { + *out = append(*out, n.Content[i+1].Value) + } + } + } + for _, c := range n.Content { + collectRefs(c, out) + } +} + +// overrideDescriptions sets or removes descriptions by JSON pointer. +func overrideDescriptions(root *yaml.Node, overrides map[string]string) error { + ptrs := make([]string, 0, len(overrides)) + for p := range overrides { + ptrs = append(ptrs, p) + } + slices.Sort(ptrs) + for _, ptr := range ptrs { + parent, key, err := resolveParent(root, ptr) + if err != nil { + return fmt.Errorf("descriptions %s: %w", ptr, err) + } + if key != "description" { + return fmt.Errorf("descriptions %s: pointer must end in /description", ptr) + } + text := overrides[ptr] + switch existing := get(parent, key); { + case text == "": + deleteKey(parent, key) + case existing != nil: + existing.Value, existing.Style = text, 0 + default: + parent.Content = append(parent.Content, str(key), str(text)) + } + } + return nil +} + +// resolveParent returns the mapping holding the last segment of ptr. +func resolveParent(root *yaml.Node, ptr string) (*yaml.Node, string, error) { + segs := strings.Split(strings.TrimPrefix(ptr, "#/"), "/") + n := root + for _, s := range segs[:len(segs)-1] { + s = strings.NewReplacer("~1", "/", "~0", "~").Replace(s) + if n = get(n, s); n == nil { + return nil, "", fmt.Errorf("segment %q not found", s) + } + } + return n, segs[len(segs)-1], nil +} + +// walkDescriptions calls fn for every description and summary. +func walkDescriptions(n *yaml.Node, ptr string, fn func(ptr, text string)) { + switch n.Kind { //nolint:exhaustive // only mappings and sequences nest + case yaml.MappingNode: + for i := 0; i < len(n.Content); i += 2 { + k, v := n.Content[i].Value, n.Content[i+1] + p := ptr + "/" + strings.NewReplacer("~", "~0", "/", "~1").Replace(k) + if (k == "description" || k == "summary" || k == "title") && v.Kind == yaml.ScalarNode { + fn("#"+p, v.Value) + continue + } + walkDescriptions(v, p, fn) + } + case yaml.SequenceNode: + for i, c := range n.Content { + walkDescriptions(c, fmt.Sprintf("%s/%d", ptr, i), fn) + } + } +} + +// propertyNames lists every property of component schemas and request +// bodies as "schemas/." or "requestBodies/.". +// Nested inline objects are reported under their component. +func propertyNames(root *yaml.Node) []string { + var out []string + comps := get(root, "components") + for _, kind := range []string{"schemas", "requestBodies"} { + items := get(comps, kind) + if items == nil { + continue + } + for i := 0; i < len(items.Content); i += 2 { + prefix := kind + "/" + items.Content[i].Value + collectProps(items.Content[i+1], prefix, &out) + } + } + return out +} + +func collectProps(n *yaml.Node, prefix string, out *[]string) { + if n.Kind != yaml.MappingNode { + for _, c := range n.Content { + collectProps(c, prefix, out) + } + return + } + for i := 0; i < len(n.Content); i += 2 { + k, v := n.Content[i].Value, n.Content[i+1] + if k == "properties" && v.Kind == yaml.MappingNode { + for j := 0; j < len(v.Content); j += 2 { + *out = append(*out, prefix+"."+v.Content[j].Value) + collectProps(v.Content[j+1], prefix, out) + } + continue + } + collectProps(v, prefix, out) + } +} + +func get(n *yaml.Node, key string) *yaml.Node { + if n == nil || n.Kind != yaml.MappingNode { + return nil + } + for i := 0; i < len(n.Content); i += 2 { + if n.Content[i].Value == key { + return n.Content[i+1] + } + } + return nil +} + +func deleteKey(n *yaml.Node, key string) { + for i := 0; i < len(n.Content); i += 2 { + if n.Content[i].Value == key { + n.Content = append(n.Content[:i], n.Content[i+2:]...) + return + } + } +} + +func keepKeys(n *yaml.Node, keys ...string) { + var kept []*yaml.Node + for i := 0; i < len(n.Content); i += 2 { + if slices.Contains(keys, n.Content[i].Value) { + kept = append(kept, n.Content[i], n.Content[i+1]) + } + } + n.Content = kept +} + +func scalar(n *yaml.Node) string { + if n == nil { + return "" + } + return n.Value +} + +func str(s string) *yaml.Node { return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: s} } diff --git a/hack/specprune/main_test.go b/hack/specprune/main_test.go new file mode 100644 index 0000000..416ab57 --- /dev/null +++ b/hack/specprune/main_test.go @@ -0,0 +1,185 @@ +package main + +import ( + "regexp" + "strings" + "testing" +) + +const source = `openapi: 3.0.0 +info: + title: Things API + version: 1.0.0 + description: Runs on the internal cluster. +servers: + - url: http://things.devlyft-apps.svc.cluster.local +security: + - JWT: [] +paths: + /v1/things/{id}: + parameters: + - name: id + in: path + required: true + schema: {type: string} + get: + operationId: GetThing + security: [{TOKEN: []}] + x-internal-note: secret + responses: + "200": + description: The thing + content: + application/json: + schema: {$ref: '#/components/schemas/Thing'} + delete: + operationId: DeleteThing + responses: + "204": {description: Deleted} + /v1/admin/things: + get: + operationId: AdminListThings + responses: + "200": + description: All things + content: + application/json: + schema: {$ref: '#/components/schemas/AdminThings'} +components: + schemas: + Thing: + type: object + required: [id, node_name] + properties: + id: {type: string} + size: + description: Size of the thing + x-go-type: kresource.Quantity + node_name: {type: string} + owner: {$ref: '#/components/schemas/Owner'} + Owner: + type: object + properties: + name: {type: string, description: Set by the identity-api.} + AdminThings: + type: object + properties: + things: {type: array, items: {$ref: '#/components/schemas/Thing'}} +` + +var ( + deny = []*regexp.Regexp{regexp.MustCompile(`(?i)identity-api|cluster\.local|\bthe internal\b`)} + suspect = []*regexp.Regexp{regexp.MustCompile(`(?i)node`)} +) + +func TestPrune(t *testing.T) { + t.Parallel() + + svc := Service{ + Operations: []string{"GetThing"}, + DropFields: []string{"schemas/Thing.node_name"}, + Descriptions: map[string]string{"#/components/schemas/Owner/properties/name/description": "Owner's display name."}, + } + out, problems, err := prune([]byte(source), &svc, deny, suspect) + if err != nil { + t.Fatal(err) + } + if len(problems) > 0 { + t.Fatalf("unexpected problems: %v", problems) + } + got := string(out) + + for _, gone := range []string{ + "DeleteThing", "AdminListThings", "AdminThings", // not allowlisted, or only reachable from them + "servers", "cluster.local", "security", "JWT", "TOKEN", // servers and security + "x-internal-note", "x-go-type", "kresource", // extensions + "node_name", // dropped field, also from required + "identity-api", "internal cluster", // overridden or dropped descriptions + } { + if strings.Contains(got, gone) { + t.Errorf("output still contains %q:\n%s", gone, got) + } + } + for _, kept := range []string{"GetThing", "Thing:", "Owner:", "Owner's display name.", "required: [id]"} { + if !strings.Contains(got, kept) { + t.Errorf("output lacks %q:\n%s", kept, got) + } + } + // A type from x-go-type becomes a string. + if !strings.Contains(got, "size:\n type: string") { + t.Errorf("size should become a string:\n%s", got) + } +} + +func TestPruneRefusesUnsafeOutput(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + svc Service + want string + }{ + { + name: "denied description", + svc: Service{Operations: []string{"GetThing"}, DropFields: []string{"schemas/Thing.node_name"}}, + want: "description at #/components/schemas/Owner/properties/name/description matches deny pattern", + }, + { + name: "suspect field not reviewed", + svc: Service{ + Operations: []string{"GetThing"}, + Descriptions: map[string]string{"#/components/schemas/Owner/properties/name/description": ""}, + }, + want: "property schemas/Thing.node_name looks internal", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + _, problems, err := prune([]byte(source), &tt.svc, deny, suspect) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(strings.Join(problems, "\n"), tt.want) { + t.Errorf("problems = %v, want one containing %q", problems, tt.want) + } + }) + } +} + +func TestPruneReviewedFieldPasses(t *testing.T) { + t.Parallel() + + svc := Service{ + Operations: []string{"GetThing"}, + Reviewed: []string{"schemas/Thing.node_name"}, + Descriptions: map[string]string{"#/components/schemas/Owner/properties/name/description": ""}, + } + _, problems, err := prune([]byte(source), &svc, deny, suspect) + if err != nil || len(problems) > 0 { + t.Errorf("err = %v, problems = %v", err, problems) + } +} + +func TestPruneErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + svc Service + want string + }{ + {"unknown operation", Service{Operations: []string{"Nope"}}, "allowlisted operation Nope not found"}, + {"unknown drop field", Service{Operations: []string{"GetThing"}, DropFields: []string{"schemas/Thing.nope"}}, "no such property"}, + {"bad override pointer", Service{Operations: []string{"GetThing"}, Descriptions: map[string]string{"#/components/schemas/Nope/description": "x"}}, "not found"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + _, _, err := prune([]byte(source), &tt.svc, deny, suspect) + if err == nil || !strings.Contains(err.Error(), tt.want) { + t.Errorf("err = %v, want %q", err, tt.want) + } + }) + } +} diff --git a/hack/vendor-specs.sh b/hack/vendor-specs.sh new file mode 100755 index 0000000..4eede3a --- /dev/null +++ b/hack/vendor-specs.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Re-vendors the pruned OpenAPI specs from the private platform repo. +# +# Usage: hack/vendor-specs.sh [REF] +# +# REF defaults to the ref in internal/api/specs/allowlist.yaml. Needs `gh` +# authenticated with read access to the platform repo. Only the files the +# allowlist names are fetched; hack/specprune decides what is published. +set -euo pipefail + +cd "$(dirname "$0")/.." +allowlist=internal/api/specs/allowlist.yaml +repo=$(awk '/^ repo:/ {print $2; exit}' "$allowlist") +current=$(awk '/^ ref:/ {print $2; exit}' "$allowlist") +ref=${1:-$current} + +if [[ "$ref" != "$current" ]]; then + # Pin the new ref in the allowlist so the vendored headers record it. + sed -i.bak "s/^ ref: .*/ ref: $ref/" "$allowlist" && rm -f "$allowlist.bak" +fi + +src=$(mktemp -d) +trap 'rm -rf "$src"' EXIT + +grep -E '^ path: ' "$allowlist" | awk '{print $2}' | while read -r path; do + mkdir -p "$src/$(dirname "$path")" + gh api "repos/$repo/contents/$path?ref=$ref" -H "Accept: application/vnd.github.raw" >"$src/$path" +done + +go run ./hack/specprune -allowlist "$allowlist" -src "$src" -out internal/api/specs +echo "Vendored specs from $repo@$ref. Review the diff before committing: this repo is public." diff --git a/internal/api/doc.go b/internal/api/doc.go deleted file mode 100644 index 14ea41c..0000000 --- a/internal/api/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package api is the thin facade over the generated DevLyft API clients. It -// is the only package that knows which service owns which endpoint. -package api diff --git a/internal/api/gen/deploys/cfg.yaml b/internal/api/gen/deploys/cfg.yaml new file mode 100644 index 0000000..2f44449 --- /dev/null +++ b/internal/api/gen/deploys/cfg.yaml @@ -0,0 +1,6 @@ +# oapi-codegen configuration; see generate.go. +package: deploys +generate: + client: true + models: true +output: deploys.gen.go diff --git a/internal/api/gen/deploys/deploys.gen.go b/internal/api/gen/deploys/deploys.gen.go new file mode 100644 index 0000000..c4acbb8 --- /dev/null +++ b/internal/api/gen/deploys/deploys.gen.go @@ -0,0 +1,2540 @@ +// Package deploys provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT. +package deploys + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/oapi-codegen/runtime" + openapi_types "github.com/oapi-codegen/runtime/types" +) + +// Defines values for CustomDomainStatus. +const ( + Active CustomDomainStatus = "active" + Failed CustomDomainStatus = "failed" + Pending CustomDomainStatus = "pending" + Revoked CustomDomainStatus = "revoked" + Verified CustomDomainStatus = "verified" +) + +// Defines values for DeployEnvVarKind. +const ( + Env DeployEnvVarKind = "env" +) + +// Defines values for DeployPlacementState. +const ( + PENDING DeployPlacementState = "PENDING" + PLACED DeployPlacementState = "PLACED" +) + +// Defines values for DeploySecretRefKind. +const ( + Secret DeploySecretRefKind = "secret" +) + +// Defines values for TenancyType. +const ( + DEDICATED TenancyType = "DEDICATED" + INHERIT TenancyType = "INHERIT" + SHARED TenancyType = "SHARED" +) + +// Build defines model for Build. +type Build struct { + // ContainerPort Port the container listens on; required to expose a deploy built from this build + ContainerPort *int `json:"container_port"` + + // CpuRequest CPU request (e.g., "100m", "1") + CpuRequest string `json:"cpu_request"` + + // CreatedAt Timestamp when the build was created + CreatedAt time.Time `json:"created_at"` + + // Id UUID of the build + Id openapi_types.UUID `json:"id"` + + // Image Container image for the build + Image string `json:"image"` + + // MemoryRequest Memory request (e.g., "128Mi", "1Gi") + MemoryRequest string `json:"memory_request"` + + // Name Name of the build + Name string `json:"name"` + + // ProjectId UUID of the parent project + ProjectId openapi_types.UUID `json:"project_id"` + + // Tag Tag for the build + Tag string `json:"tag"` + + // UpdatedAt Timestamp when the build was last updated + UpdatedAt time.Time `json:"updated_at"` +} + +// CustomDomain A customer-owned domain and its ownership-verification state. +type CustomDomain struct { + // ChallengeRecordName DNS name to create a TXT record at (its value must equal verification_token) + ChallengeRecordName string `json:"challenge_record_name"` + CreatedAt time.Time `json:"created_at"` + + // Domain The owned domain; verification covers it and all of its subdomains + Domain string `json:"domain"` + + // Id UUID of the custom domain + Id openapi_types.UUID `json:"id"` + + // LastCheckedAt When verification was last attempted + LastCheckedAt *time.Time `json:"last_checked_at"` + + // OrganizationId UUID of the owning organization + OrganizationId openapi_types.UUID `json:"organization_id"` + + // Status Ownership-verification state + Status CustomDomainStatus `json:"status"` + UpdatedAt time.Time `json:"updated_at"` + + // VerificationToken Value to publish in the TXT challenge record + VerificationToken string `json:"verification_token"` + + // VerifiedAt When ownership was first verified + VerifiedAt *time.Time `json:"verified_at"` +} + +// CustomDomainStatus Ownership-verification state +type CustomDomainStatus string + +// Deploy defines model for Deploy. +type Deploy struct { + // BuildId UUID of the build used by this deploy + BuildId openapi_types.UUID `json:"build_id"` + + // CreatedAt Timestamp when the deploy was created + CreatedAt time.Time `json:"created_at"` + + // EnableHpa Whether HPA is enabled + EnableHpa bool `json:"enable_hpa"` + + // Env Env entries (plain vars or secret refs). + Env *[]DeployEnvItem `json:"env"` + + // EnvironmentId UUID of the parent environment + EnvironmentId openapi_types.UUID `json:"environment_id"` + + // Expose Whether the deploy is publicly routed via edge ingress + Expose bool `json:"expose"` + + // HasRegistryCredential Whether a private container registry pull credential is set for this deploy's image. The credential value itself is never returned. + HasRegistryCredential bool `json:"has_registry_credential"` + + // HpaMaxReplicas Maximum replicas for HPA + HpaMaxReplicas *uint32 `json:"hpa_max_replicas"` + + // HpaMinReplicas Minimum replicas for HPA + HpaMinReplicas *uint32 `json:"hpa_min_replicas"` + + // HpaTargetCpu Target CPU percentage for HPA + HpaTargetCpu *uint32 `json:"hpa_target_cpu"` + + // Id UUID of the deploy + Id openapi_types.UUID `json:"id"` + + // InstanceType Dedicated node instance type (present when the deploy pins one). + InstanceType *string `json:"instance_type,omitempty"` + + // LocationId Location ID where the deploy is deployed + LocationId string `json:"location_id"` + + // Name Name of the deploy + Name string `json:"name"` + + // Placement Where the deploy is running. All deploys of one environment in one location share a cluster, so this is a property of the environment's placement as much as the deploy's. + Placement DeployPlacement `json:"placement"` + + // RegistryCredentialName Name of the saved registry credential this deploy uses. Null when it has none. + RegistryCredentialName *string `json:"registry_credential_name"` + + // Replicas Number of replicas + Replicas uint32 `json:"replicas"` + + // TenancyType Type of tenancy for the deploy. INHERIT follows the environment's tenancy policy; SHARED forces the global shared pool; DEDICATED leases nodes to this deploy exclusively. + TenancyType TenancyType `json:"tenancy_type"` + + // UpdatedAt Timestamp when the deploy was last updated + UpdatedAt time.Time `json:"updated_at"` +} + +// DeployEnvItem One env entry — user specifies kind (env or secret) to indicate plain value vs secret ref. +type DeployEnvItem struct { + union json.RawMessage +} + +// DeployEnvVar Environment variable with a plain value. +type DeployEnvVar struct { + // Key Variable name + Key string `json:"key"` + + // Kind Identifies this entry as an env var + Kind DeployEnvVarKind `json:"kind"` + + // Value Plain value + Value string `json:"value"` +} + +// DeployEnvVarKind Identifies this entry as an env var +type DeployEnvVarKind string + +// DeployPlacement Where the deploy is running. All deploys of one environment in one location share a cluster, so this is a property of the environment's placement as much as the deploy's. +type DeployPlacement struct { + // ClusterName The name of the edge cluster serving this deploy. Absent while state is PENDING. + ClusterName *string `json:"cluster_name,omitempty"` + + // State Whether the deploy has been assigned to an edge cluster. PENDING means no cluster in the deploy's location can currently take it, most often because the cluster its environment occupies is full or is being drained. A deploy stays PENDING until capacity frees up; it is retried automatically and needs no action to resume. + State DeployPlacementState `json:"state"` +} + +// DeployPlacementState Whether the deploy has been assigned to an edge cluster. PENDING means no cluster in the deploy's location can currently take it, most often because the cluster its environment occupies is full or is being drained. A deploy stays PENDING until capacity frees up; it is retried automatically and needs no action to resume. +type DeployPlacementState string + +// DeploySecretRef Reference to an environment secret by key; the variable takes the secret's value. +type DeploySecretRef struct { + // Key Secret key name + Key string `json:"key"` + + // Kind Identifies this entry as a secret ref + Kind DeploySecretRefKind `json:"kind"` +} + +// DeploySecretRefKind Identifies this entry as a secret ref +type DeploySecretRefKind string + +// ListBuildsResponse defines model for ListBuildsResponse. +type ListBuildsResponse struct { + Builds []Build `json:"builds"` +} + +// ListCustomDomainsResponse defines model for ListCustomDomainsResponse. +type ListCustomDomainsResponse struct { + Domains []CustomDomain `json:"domains"` +} + +// ListDeploysResponse defines model for ListDeploysResponse. +type ListDeploysResponse struct { + Deploys []Deploy `json:"deploys"` +} + +// ListRoutesResponse defines model for ListRoutesResponse. +type ListRoutesResponse struct { + Routes []Route `json:"routes"` +} + +// Route A hostname (optionally scoped to a path prefix) routed to a deploy. +type Route struct { + // CnameTarget The name to point this hostname's CNAME at. It is derived from the hostname and never changes for the life of the route, including when the deploy moves between edge clusters — which is the point: the customer publishes this once and never edits their DNS again. Absent when the deployment publishes no edge DNS. + CnameTarget *string `json:"cname_target,omitempty"` + CreatedAt time.Time `json:"created_at"` + DeployId openapi_types.UUID `json:"deploy_id"` + Hostname string `json:"hostname"` + Id openapi_types.UUID `json:"id"` + PathPrefix string `json:"path_prefix"` + UpdatedAt time.Time `json:"updated_at"` +} + +// TenancyType Type of tenancy for the deploy. INHERIT follows the environment's tenancy policy; SHARED forces the global shared pool; DEDICATED leases nodes to this deploy exclusively. +type TenancyType string + +// CreateBuildRequest defines model for CreateBuildRequest. +type CreateBuildRequest struct { + // ContainerPort Port the container listens on; required to expose a deploy built from this build + ContainerPort *int `json:"container_port"` + + // CpuRequest CPU request (e.g., "100m", "1") + CpuRequest string `json:"cpu_request"` + + // Image Container image for the build + Image string `json:"image"` + + // MemoryRequest Memory request (e.g., "128Mi", "1Gi") + MemoryRequest string `json:"memory_request"` + + // Name Name of the build + Name string `json:"name"` + + // Tag Tag for the build + Tag string `json:"tag"` +} + +// CreateCustomDomainRequest defines model for CreateCustomDomainRequest. +type CreateCustomDomainRequest struct { + // Domain The domain the organization owns and can prove control of, e.g. "devlyft.io". Ownership is verified once (via a TXT record at _devlyft-challenge.) and covers this domain and all of its subdomains, which may then be routed to deploys. + Domain string `json:"domain"` +} + +// CreateDeployRequest defines model for CreateDeployRequest. +type CreateDeployRequest struct { + // BuildId UUID of the build to use + BuildId openapi_types.UUID `json:"build_id"` + + // EnableHpa Whether HPA is enabled + EnableHpa bool `json:"enable_hpa"` + + // Env Env entries (plain vars or secret refs) + Env *[]DeployEnvItem `json:"env"` + + // Expose Publicly route this deploy via edge ingress (requires the build to declare a container_port) + Expose *bool `json:"expose,omitempty"` + + // HpaMaxReplicas Maximum replicas for HPA + HpaMaxReplicas *uint32 `json:"hpa_max_replicas"` + + // HpaMinReplicas Minimum replicas for HPA + HpaMinReplicas *uint32 `json:"hpa_min_replicas"` + + // HpaTargetCpu Target CPU percentage for HPA + HpaTargetCpu *uint32 `json:"hpa_target_cpu"` + + // InstanceType Dedicated node instance type (e.g. devlyft-amd-balanced-2). Required when the deploy is DEDICATED; INHERIT deploys inherit the environment's. + InstanceType *string `json:"instance_type,omitempty"` + + // LocationId Location ID where the deploy is deployed + LocationId string `json:"location_id"` + + // Name Name of the deploy + Name string `json:"name"` + + // RegistryCredentialName Name of a registry credential saved in the deploy's environment, used to pull a private image. Omit for a public image. + RegistryCredentialName *string `json:"registry_credential_name,omitempty"` + + // Replicas Number of replicas + Replicas uint32 `json:"replicas"` + + // TenancyType Type of tenancy for the deploy. INHERIT follows the environment's tenancy policy; SHARED forces the global shared pool; DEDICATED leases nodes to this deploy exclusively. + TenancyType *TenancyType `json:"tenancy_type,omitempty"` +} + +// CreateRouteRequest defines model for CreateRouteRequest. +type CreateRouteRequest struct { + // Hostname Hostname to route; must be covered by an org-verified domain + Hostname string `json:"hostname"` + + // PathPrefix Optional path prefix (e.g. /v1/one); omit or "/" for the whole host + PathPrefix *string `json:"path_prefix,omitempty"` +} + +// UpdateDeployRequest defines model for UpdateDeployRequest. +type UpdateDeployRequest struct { + // BuildId UUID of the build to use + BuildId openapi_types.UUID `json:"build_id"` + + // EnableHpa Whether HPA is enabled + EnableHpa bool `json:"enable_hpa"` + + // Env Env entries (plain vars or secret refs) + Env *[]DeployEnvItem `json:"env"` + + // Expose Publicly route this deploy via edge ingress (requires the build to declare a container_port) + Expose *bool `json:"expose,omitempty"` + + // HpaMaxReplicas Maximum replicas for HPA + HpaMaxReplicas *uint32 `json:"hpa_max_replicas"` + + // HpaMinReplicas Minimum replicas for HPA + HpaMinReplicas *uint32 `json:"hpa_min_replicas"` + + // HpaTargetCpu Target CPU percentage for HPA + HpaTargetCpu *uint32 `json:"hpa_target_cpu"` + + // InstanceType Dedicated node instance type (e.g. devlyft-amd-balanced-2). Required when the deploy is DEDICATED; INHERIT deploys inherit the environment's. + InstanceType *string `json:"instance_type,omitempty"` + + // LocationId Location ID where the deploy is deployed + LocationId string `json:"location_id"` + + // Name Name of the deploy + Name string `json:"name"` + + // RegistryCredentialName Name of a registry credential saved in the deploy's environment, used to pull a private image. Omit to keep the deploy's current credential; detach one with DELETE /v1/deploys/{deploy_id}/registry-credential. + RegistryCredentialName *string `json:"registry_credential_name,omitempty"` + + // Replicas Number of replicas + Replicas uint32 `json:"replicas"` + + // TenancyType Type of tenancy for the deploy. INHERIT follows the environment's tenancy policy; SHARED forces the global shared pool; DEDICATED leases nodes to this deploy exclusively. + TenancyType *TenancyType `json:"tenancy_type,omitempty"` +} + +// PutV1DeploysDeployIdJSONBody defines parameters for PutV1DeploysDeployId. +type PutV1DeploysDeployIdJSONBody struct { + // BuildId UUID of the build to use + BuildId openapi_types.UUID `json:"build_id"` + + // EnableHpa Whether HPA is enabled + EnableHpa bool `json:"enable_hpa"` + + // Env Env entries (plain vars or secret refs) + Env *[]DeployEnvItem `json:"env"` + + // Expose Publicly route this deploy via edge ingress (requires the build to declare a container_port) + Expose *bool `json:"expose,omitempty"` + + // HpaMaxReplicas Maximum replicas for HPA + HpaMaxReplicas *uint32 `json:"hpa_max_replicas"` + + // HpaMinReplicas Minimum replicas for HPA + HpaMinReplicas *uint32 `json:"hpa_min_replicas"` + + // HpaTargetCpu Target CPU percentage for HPA + HpaTargetCpu *uint32 `json:"hpa_target_cpu"` + + // InstanceType Dedicated node instance type (e.g. devlyft-amd-balanced-2). Required when the deploy is DEDICATED; INHERIT deploys inherit the environment's. + InstanceType *string `json:"instance_type,omitempty"` + + // LocationId Location ID where the deploy is deployed + LocationId string `json:"location_id"` + + // Name Name of the deploy + Name string `json:"name"` + + // RegistryCredentialName Name of a registry credential saved in the deploy's environment, used to pull a private image. Omit to keep the deploy's current credential; detach one with DELETE /v1/deploys/{deploy_id}/registry-credential. + RegistryCredentialName *string `json:"registry_credential_name,omitempty"` + + // Replicas Number of replicas + Replicas uint32 `json:"replicas"` + + // TenancyType Type of tenancy for the deploy. INHERIT follows the environment's tenancy policy; SHARED forces the global shared pool; DEDICATED leases nodes to this deploy exclusively. + TenancyType *TenancyType `json:"tenancy_type,omitempty"` +} + +// PostV1DeploysDeployIdRoutesJSONBody defines parameters for PostV1DeploysDeployIdRoutes. +type PostV1DeploysDeployIdRoutesJSONBody struct { + // Hostname Hostname to route; must be covered by an org-verified domain + Hostname string `json:"hostname"` + + // PathPrefix Optional path prefix (e.g. /v1/one); omit or "/" for the whole host + PathPrefix *string `json:"path_prefix,omitempty"` +} + +// PostV1OrganizationsOrganizationIdDomainsJSONBody defines parameters for PostV1OrganizationsOrganizationIdDomains. +type PostV1OrganizationsOrganizationIdDomainsJSONBody struct { + // Domain The domain the organization owns and can prove control of, e.g. "devlyft.io". Ownership is verified once (via a TXT record at _devlyft-challenge.) and covers this domain and all of its subdomains, which may then be routed to deploys. + Domain string `json:"domain"` +} + +// PostV1ProjectsProjectIdBuildsJSONBody defines parameters for PostV1ProjectsProjectIdBuilds. +type PostV1ProjectsProjectIdBuildsJSONBody struct { + // ContainerPort Port the container listens on; required to expose a deploy built from this build + ContainerPort *int `json:"container_port"` + + // CpuRequest CPU request (e.g., "100m", "1") + CpuRequest string `json:"cpu_request"` + + // Image Container image for the build + Image string `json:"image"` + + // MemoryRequest Memory request (e.g., "128Mi", "1Gi") + MemoryRequest string `json:"memory_request"` + + // Name Name of the build + Name string `json:"name"` + + // Tag Tag for the build + Tag string `json:"tag"` +} + +// PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysJSONBody defines parameters for PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys. +type PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysJSONBody struct { + // BuildId UUID of the build to use + BuildId openapi_types.UUID `json:"build_id"` + + // EnableHpa Whether HPA is enabled + EnableHpa bool `json:"enable_hpa"` + + // Env Env entries (plain vars or secret refs) + Env *[]DeployEnvItem `json:"env"` + + // Expose Publicly route this deploy via edge ingress (requires the build to declare a container_port) + Expose *bool `json:"expose,omitempty"` + + // HpaMaxReplicas Maximum replicas for HPA + HpaMaxReplicas *uint32 `json:"hpa_max_replicas"` + + // HpaMinReplicas Minimum replicas for HPA + HpaMinReplicas *uint32 `json:"hpa_min_replicas"` + + // HpaTargetCpu Target CPU percentage for HPA + HpaTargetCpu *uint32 `json:"hpa_target_cpu"` + + // InstanceType Dedicated node instance type (e.g. devlyft-amd-balanced-2). Required when the deploy is DEDICATED; INHERIT deploys inherit the environment's. + InstanceType *string `json:"instance_type,omitempty"` + + // LocationId Location ID where the deploy is deployed + LocationId string `json:"location_id"` + + // Name Name of the deploy + Name string `json:"name"` + + // RegistryCredentialName Name of a registry credential saved in the deploy's environment, used to pull a private image. Omit for a public image. + RegistryCredentialName *string `json:"registry_credential_name,omitempty"` + + // Replicas Number of replicas + Replicas uint32 `json:"replicas"` + + // TenancyType Type of tenancy for the deploy. INHERIT follows the environment's tenancy policy; SHARED forces the global shared pool; DEDICATED leases nodes to this deploy exclusively. + TenancyType *TenancyType `json:"tenancy_type,omitempty"` +} + +// PutV1DeploysDeployIdJSONRequestBody defines body for PutV1DeploysDeployId for application/json ContentType. +type PutV1DeploysDeployIdJSONRequestBody PutV1DeploysDeployIdJSONBody + +// PostV1DeploysDeployIdRoutesJSONRequestBody defines body for PostV1DeploysDeployIdRoutes for application/json ContentType. +type PostV1DeploysDeployIdRoutesJSONRequestBody PostV1DeploysDeployIdRoutesJSONBody + +// PostV1OrganizationsOrganizationIdDomainsJSONRequestBody defines body for PostV1OrganizationsOrganizationIdDomains for application/json ContentType. +type PostV1OrganizationsOrganizationIdDomainsJSONRequestBody PostV1OrganizationsOrganizationIdDomainsJSONBody + +// PostV1ProjectsProjectIdBuildsJSONRequestBody defines body for PostV1ProjectsProjectIdBuilds for application/json ContentType. +type PostV1ProjectsProjectIdBuildsJSONRequestBody PostV1ProjectsProjectIdBuildsJSONBody + +// PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysJSONRequestBody defines body for PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys for application/json ContentType. +type PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysJSONRequestBody PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysJSONBody + +// AsDeployEnvVar returns the union data inside the DeployEnvItem as a DeployEnvVar +func (t DeployEnvItem) AsDeployEnvVar() (DeployEnvVar, error) { + var body DeployEnvVar + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromDeployEnvVar overwrites any union data inside the DeployEnvItem as the provided DeployEnvVar +func (t *DeployEnvItem) FromDeployEnvVar(v DeployEnvVar) error { + v.Kind = "DeployEnvVar" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeDeployEnvVar performs a merge with any union data inside the DeployEnvItem, using the provided DeployEnvVar +func (t *DeployEnvItem) MergeDeployEnvVar(v DeployEnvVar) error { + v.Kind = "DeployEnvVar" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsDeploySecretRef returns the union data inside the DeployEnvItem as a DeploySecretRef +func (t DeployEnvItem) AsDeploySecretRef() (DeploySecretRef, error) { + var body DeploySecretRef + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromDeploySecretRef overwrites any union data inside the DeployEnvItem as the provided DeploySecretRef +func (t *DeployEnvItem) FromDeploySecretRef(v DeploySecretRef) error { + v.Kind = "DeploySecretRef" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeDeploySecretRef performs a merge with any union data inside the DeployEnvItem, using the provided DeploySecretRef +func (t *DeployEnvItem) MergeDeploySecretRef(v DeploySecretRef) error { + v.Kind = "DeploySecretRef" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t DeployEnvItem) Discriminator() (string, error) { + var discriminator struct { + Discriminator string `json:"kind"` + } + err := json.Unmarshal(t.union, &discriminator) + return discriminator.Discriminator, err +} + +func (t DeployEnvItem) ValueByDiscriminator() (interface{}, error) { + discriminator, err := t.Discriminator() + if err != nil { + return nil, err + } + switch discriminator { + case "DeployEnvVar": + return t.AsDeployEnvVar() + case "DeploySecretRef": + return t.AsDeploySecretRef() + default: + return nil, errors.New("unknown discriminator value: " + discriminator) + } +} + +func (t DeployEnvItem) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *DeployEnvItem) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // GetV1BuildsBuildId request + GetV1BuildsBuildId(ctx context.Context, buildId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteV1DeploysDeployId request + DeleteV1DeploysDeployId(ctx context.Context, deployId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1DeploysDeployId request + GetV1DeploysDeployId(ctx context.Context, deployId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PutV1DeploysDeployIdWithBody request with any body + PutV1DeploysDeployIdWithBody(ctx context.Context, deployId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + PutV1DeploysDeployId(ctx context.Context, deployId openapi_types.UUID, body PutV1DeploysDeployIdJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1DeploysDeployIdRoutes request + GetV1DeploysDeployIdRoutes(ctx context.Context, deployId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PostV1DeploysDeployIdRoutesWithBody request with any body + PostV1DeploysDeployIdRoutesWithBody(ctx context.Context, deployId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + PostV1DeploysDeployIdRoutes(ctx context.Context, deployId openapi_types.UUID, body PostV1DeploysDeployIdRoutesJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteV1DeploysDeployIdRoutesRouteId request + DeleteV1DeploysDeployIdRoutesRouteId(ctx context.Context, deployId openapi_types.UUID, routeId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1OrganizationsOrganizationIdDomains request + GetV1OrganizationsOrganizationIdDomains(ctx context.Context, organizationId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PostV1OrganizationsOrganizationIdDomainsWithBody request with any body + PostV1OrganizationsOrganizationIdDomainsWithBody(ctx context.Context, organizationId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + PostV1OrganizationsOrganizationIdDomains(ctx context.Context, organizationId openapi_types.UUID, body PostV1OrganizationsOrganizationIdDomainsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteV1OrganizationsOrganizationIdDomainsDomain request + DeleteV1OrganizationsOrganizationIdDomainsDomain(ctx context.Context, organizationId openapi_types.UUID, domain string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PostV1OrganizationsOrganizationIdDomainsDomainVerify request + PostV1OrganizationsOrganizationIdDomainsDomainVerify(ctx context.Context, organizationId openapi_types.UUID, domain string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1ProjectsProjectIdBuilds request + GetV1ProjectsProjectIdBuilds(ctx context.Context, projectId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PostV1ProjectsProjectIdBuildsWithBody request with any body + PostV1ProjectsProjectIdBuildsWithBody(ctx context.Context, projectId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + PostV1ProjectsProjectIdBuilds(ctx context.Context, projectId openapi_types.UUID, body PostV1ProjectsProjectIdBuildsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys request + GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithBody request with any body + PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithBody(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, body PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) GetV1BuildsBuildId(ctx context.Context, buildId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1BuildsBuildIdRequest(c.Server, buildId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DeleteV1DeploysDeployId(ctx context.Context, deployId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteV1DeploysDeployIdRequest(c.Server, deployId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1DeploysDeployId(ctx context.Context, deployId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1DeploysDeployIdRequest(c.Server, deployId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PutV1DeploysDeployIdWithBody(ctx context.Context, deployId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPutV1DeploysDeployIdRequestWithBody(c.Server, deployId, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PutV1DeploysDeployId(ctx context.Context, deployId openapi_types.UUID, body PutV1DeploysDeployIdJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPutV1DeploysDeployIdRequest(c.Server, deployId, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1DeploysDeployIdRoutes(ctx context.Context, deployId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1DeploysDeployIdRoutesRequest(c.Server, deployId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PostV1DeploysDeployIdRoutesWithBody(ctx context.Context, deployId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostV1DeploysDeployIdRoutesRequestWithBody(c.Server, deployId, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PostV1DeploysDeployIdRoutes(ctx context.Context, deployId openapi_types.UUID, body PostV1DeploysDeployIdRoutesJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostV1DeploysDeployIdRoutesRequest(c.Server, deployId, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DeleteV1DeploysDeployIdRoutesRouteId(ctx context.Context, deployId openapi_types.UUID, routeId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteV1DeploysDeployIdRoutesRouteIdRequest(c.Server, deployId, routeId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1OrganizationsOrganizationIdDomains(ctx context.Context, organizationId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1OrganizationsOrganizationIdDomainsRequest(c.Server, organizationId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PostV1OrganizationsOrganizationIdDomainsWithBody(ctx context.Context, organizationId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostV1OrganizationsOrganizationIdDomainsRequestWithBody(c.Server, organizationId, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PostV1OrganizationsOrganizationIdDomains(ctx context.Context, organizationId openapi_types.UUID, body PostV1OrganizationsOrganizationIdDomainsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostV1OrganizationsOrganizationIdDomainsRequest(c.Server, organizationId, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DeleteV1OrganizationsOrganizationIdDomainsDomain(ctx context.Context, organizationId openapi_types.UUID, domain string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteV1OrganizationsOrganizationIdDomainsDomainRequest(c.Server, organizationId, domain) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PostV1OrganizationsOrganizationIdDomainsDomainVerify(ctx context.Context, organizationId openapi_types.UUID, domain string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostV1OrganizationsOrganizationIdDomainsDomainVerifyRequest(c.Server, organizationId, domain) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1ProjectsProjectIdBuilds(ctx context.Context, projectId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1ProjectsProjectIdBuildsRequest(c.Server, projectId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PostV1ProjectsProjectIdBuildsWithBody(ctx context.Context, projectId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostV1ProjectsProjectIdBuildsRequestWithBody(c.Server, projectId, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PostV1ProjectsProjectIdBuilds(ctx context.Context, projectId openapi_types.UUID, body PostV1ProjectsProjectIdBuildsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostV1ProjectsProjectIdBuildsRequest(c.Server, projectId, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysRequest(c.Server, projectId, environmentId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithBody(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysRequestWithBody(c.Server, projectId, environmentId, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, body PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysRequest(c.Server, projectId, environmentId, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewGetV1BuildsBuildIdRequest generates requests for GetV1BuildsBuildId +func NewGetV1BuildsBuildIdRequest(server string, buildId openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "build_id", runtime.ParamLocationPath, buildId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/builds/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewDeleteV1DeploysDeployIdRequest generates requests for DeleteV1DeploysDeployId +func NewDeleteV1DeploysDeployIdRequest(server string, deployId openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "deploy_id", runtime.ParamLocationPath, deployId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/deploys/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetV1DeploysDeployIdRequest generates requests for GetV1DeploysDeployId +func NewGetV1DeploysDeployIdRequest(server string, deployId openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "deploy_id", runtime.ParamLocationPath, deployId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/deploys/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewPutV1DeploysDeployIdRequest calls the generic PutV1DeploysDeployId builder with application/json body +func NewPutV1DeploysDeployIdRequest(server string, deployId openapi_types.UUID, body PutV1DeploysDeployIdJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPutV1DeploysDeployIdRequestWithBody(server, deployId, "application/json", bodyReader) +} + +// NewPutV1DeploysDeployIdRequestWithBody generates requests for PutV1DeploysDeployId with any type of body +func NewPutV1DeploysDeployIdRequestWithBody(server string, deployId openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "deploy_id", runtime.ParamLocationPath, deployId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/deploys/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewGetV1DeploysDeployIdRoutesRequest generates requests for GetV1DeploysDeployIdRoutes +func NewGetV1DeploysDeployIdRoutesRequest(server string, deployId openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "deploy_id", runtime.ParamLocationPath, deployId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/deploys/%s/routes", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewPostV1DeploysDeployIdRoutesRequest calls the generic PostV1DeploysDeployIdRoutes builder with application/json body +func NewPostV1DeploysDeployIdRoutesRequest(server string, deployId openapi_types.UUID, body PostV1DeploysDeployIdRoutesJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPostV1DeploysDeployIdRoutesRequestWithBody(server, deployId, "application/json", bodyReader) +} + +// NewPostV1DeploysDeployIdRoutesRequestWithBody generates requests for PostV1DeploysDeployIdRoutes with any type of body +func NewPostV1DeploysDeployIdRoutesRequestWithBody(server string, deployId openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "deploy_id", runtime.ParamLocationPath, deployId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/deploys/%s/routes", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewDeleteV1DeploysDeployIdRoutesRouteIdRequest generates requests for DeleteV1DeploysDeployIdRoutesRouteId +func NewDeleteV1DeploysDeployIdRoutesRouteIdRequest(server string, deployId openapi_types.UUID, routeId openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "deploy_id", runtime.ParamLocationPath, deployId) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "route_id", runtime.ParamLocationPath, routeId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/deploys/%s/routes/%s", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetV1OrganizationsOrganizationIdDomainsRequest generates requests for GetV1OrganizationsOrganizationIdDomains +func NewGetV1OrganizationsOrganizationIdDomainsRequest(server string, organizationId openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "organization_id", runtime.ParamLocationPath, organizationId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/organizations/%s/domains", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewPostV1OrganizationsOrganizationIdDomainsRequest calls the generic PostV1OrganizationsOrganizationIdDomains builder with application/json body +func NewPostV1OrganizationsOrganizationIdDomainsRequest(server string, organizationId openapi_types.UUID, body PostV1OrganizationsOrganizationIdDomainsJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPostV1OrganizationsOrganizationIdDomainsRequestWithBody(server, organizationId, "application/json", bodyReader) +} + +// NewPostV1OrganizationsOrganizationIdDomainsRequestWithBody generates requests for PostV1OrganizationsOrganizationIdDomains with any type of body +func NewPostV1OrganizationsOrganizationIdDomainsRequestWithBody(server string, organizationId openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "organization_id", runtime.ParamLocationPath, organizationId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/organizations/%s/domains", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewDeleteV1OrganizationsOrganizationIdDomainsDomainRequest generates requests for DeleteV1OrganizationsOrganizationIdDomainsDomain +func NewDeleteV1OrganizationsOrganizationIdDomainsDomainRequest(server string, organizationId openapi_types.UUID, domain string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "organization_id", runtime.ParamLocationPath, organizationId) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "domain", runtime.ParamLocationPath, domain) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/organizations/%s/domains/%s", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewPostV1OrganizationsOrganizationIdDomainsDomainVerifyRequest generates requests for PostV1OrganizationsOrganizationIdDomainsDomainVerify +func NewPostV1OrganizationsOrganizationIdDomainsDomainVerifyRequest(server string, organizationId openapi_types.UUID, domain string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "organization_id", runtime.ParamLocationPath, organizationId) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "domain", runtime.ParamLocationPath, domain) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/organizations/%s/domains/%s/verify", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetV1ProjectsProjectIdBuildsRequest generates requests for GetV1ProjectsProjectIdBuilds +func NewGetV1ProjectsProjectIdBuildsRequest(server string, projectId openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "project_id", runtime.ParamLocationPath, projectId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/builds", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewPostV1ProjectsProjectIdBuildsRequest calls the generic PostV1ProjectsProjectIdBuilds builder with application/json body +func NewPostV1ProjectsProjectIdBuildsRequest(server string, projectId openapi_types.UUID, body PostV1ProjectsProjectIdBuildsJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPostV1ProjectsProjectIdBuildsRequestWithBody(server, projectId, "application/json", bodyReader) +} + +// NewPostV1ProjectsProjectIdBuildsRequestWithBody generates requests for PostV1ProjectsProjectIdBuilds with any type of body +func NewPostV1ProjectsProjectIdBuildsRequestWithBody(server string, projectId openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "project_id", runtime.ParamLocationPath, projectId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/builds", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewGetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysRequest generates requests for GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys +func NewGetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysRequest(server string, projectId openapi_types.UUID, environmentId openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "project_id", runtime.ParamLocationPath, projectId) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "environment_id", runtime.ParamLocationPath, environmentId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/environments/%s/deploys", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewPostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysRequest calls the generic PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys builder with application/json body +func NewPostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysRequest(server string, projectId openapi_types.UUID, environmentId openapi_types.UUID, body PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysRequestWithBody(server, projectId, environmentId, "application/json", bodyReader) +} + +// NewPostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysRequestWithBody generates requests for PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys with any type of body +func NewPostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysRequestWithBody(server string, projectId openapi_types.UUID, environmentId openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "project_id", runtime.ParamLocationPath, projectId) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "environment_id", runtime.ParamLocationPath, environmentId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/environments/%s/deploys", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // GetV1BuildsBuildIdWithResponse request + GetV1BuildsBuildIdWithResponse(ctx context.Context, buildId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1BuildsBuildIdResponse, error) + + // DeleteV1DeploysDeployIdWithResponse request + DeleteV1DeploysDeployIdWithResponse(ctx context.Context, deployId openapi_types.UUID, reqEditors ...RequestEditorFn) (*DeleteV1DeploysDeployIdResponse, error) + + // GetV1DeploysDeployIdWithResponse request + GetV1DeploysDeployIdWithResponse(ctx context.Context, deployId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1DeploysDeployIdResponse, error) + + // PutV1DeploysDeployIdWithBodyWithResponse request with any body + PutV1DeploysDeployIdWithBodyWithResponse(ctx context.Context, deployId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PutV1DeploysDeployIdResponse, error) + + PutV1DeploysDeployIdWithResponse(ctx context.Context, deployId openapi_types.UUID, body PutV1DeploysDeployIdJSONRequestBody, reqEditors ...RequestEditorFn) (*PutV1DeploysDeployIdResponse, error) + + // GetV1DeploysDeployIdRoutesWithResponse request + GetV1DeploysDeployIdRoutesWithResponse(ctx context.Context, deployId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1DeploysDeployIdRoutesResponse, error) + + // PostV1DeploysDeployIdRoutesWithBodyWithResponse request with any body + PostV1DeploysDeployIdRoutesWithBodyWithResponse(ctx context.Context, deployId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostV1DeploysDeployIdRoutesResponse, error) + + PostV1DeploysDeployIdRoutesWithResponse(ctx context.Context, deployId openapi_types.UUID, body PostV1DeploysDeployIdRoutesJSONRequestBody, reqEditors ...RequestEditorFn) (*PostV1DeploysDeployIdRoutesResponse, error) + + // DeleteV1DeploysDeployIdRoutesRouteIdWithResponse request + DeleteV1DeploysDeployIdRoutesRouteIdWithResponse(ctx context.Context, deployId openapi_types.UUID, routeId openapi_types.UUID, reqEditors ...RequestEditorFn) (*DeleteV1DeploysDeployIdRoutesRouteIdResponse, error) + + // GetV1OrganizationsOrganizationIdDomainsWithResponse request + GetV1OrganizationsOrganizationIdDomainsWithResponse(ctx context.Context, organizationId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1OrganizationsOrganizationIdDomainsResponse, error) + + // PostV1OrganizationsOrganizationIdDomainsWithBodyWithResponse request with any body + PostV1OrganizationsOrganizationIdDomainsWithBodyWithResponse(ctx context.Context, organizationId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostV1OrganizationsOrganizationIdDomainsResponse, error) + + PostV1OrganizationsOrganizationIdDomainsWithResponse(ctx context.Context, organizationId openapi_types.UUID, body PostV1OrganizationsOrganizationIdDomainsJSONRequestBody, reqEditors ...RequestEditorFn) (*PostV1OrganizationsOrganizationIdDomainsResponse, error) + + // DeleteV1OrganizationsOrganizationIdDomainsDomainWithResponse request + DeleteV1OrganizationsOrganizationIdDomainsDomainWithResponse(ctx context.Context, organizationId openapi_types.UUID, domain string, reqEditors ...RequestEditorFn) (*DeleteV1OrganizationsOrganizationIdDomainsDomainResponse, error) + + // PostV1OrganizationsOrganizationIdDomainsDomainVerifyWithResponse request + PostV1OrganizationsOrganizationIdDomainsDomainVerifyWithResponse(ctx context.Context, organizationId openapi_types.UUID, domain string, reqEditors ...RequestEditorFn) (*PostV1OrganizationsOrganizationIdDomainsDomainVerifyResponse, error) + + // GetV1ProjectsProjectIdBuildsWithResponse request + GetV1ProjectsProjectIdBuildsWithResponse(ctx context.Context, projectId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1ProjectsProjectIdBuildsResponse, error) + + // PostV1ProjectsProjectIdBuildsWithBodyWithResponse request with any body + PostV1ProjectsProjectIdBuildsWithBodyWithResponse(ctx context.Context, projectId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostV1ProjectsProjectIdBuildsResponse, error) + + PostV1ProjectsProjectIdBuildsWithResponse(ctx context.Context, projectId openapi_types.UUID, body PostV1ProjectsProjectIdBuildsJSONRequestBody, reqEditors ...RequestEditorFn) (*PostV1ProjectsProjectIdBuildsResponse, error) + + // GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithResponse request + GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithResponse(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse, error) + + // PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithBodyWithResponse request with any body + PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithBodyWithResponse(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse, error) + + PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithResponse(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, body PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysJSONRequestBody, reqEditors ...RequestEditorFn) (*PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse, error) +} + +type GetV1BuildsBuildIdResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Build +} + +// Status returns HTTPResponse.Status +func (r GetV1BuildsBuildIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1BuildsBuildIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DeleteV1DeploysDeployIdResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r DeleteV1DeploysDeployIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteV1DeploysDeployIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1DeploysDeployIdResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Deploy +} + +// Status returns HTTPResponse.Status +func (r GetV1DeploysDeployIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1DeploysDeployIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PutV1DeploysDeployIdResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Deploy +} + +// Status returns HTTPResponse.Status +func (r PutV1DeploysDeployIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PutV1DeploysDeployIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1DeploysDeployIdRoutesResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ListRoutesResponse +} + +// Status returns HTTPResponse.Status +func (r GetV1DeploysDeployIdRoutesResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1DeploysDeployIdRoutesResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PostV1DeploysDeployIdRoutesResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *Route +} + +// Status returns HTTPResponse.Status +func (r PostV1DeploysDeployIdRoutesResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PostV1DeploysDeployIdRoutesResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DeleteV1DeploysDeployIdRoutesRouteIdResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r DeleteV1DeploysDeployIdRoutesRouteIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteV1DeploysDeployIdRoutesRouteIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1OrganizationsOrganizationIdDomainsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ListCustomDomainsResponse +} + +// Status returns HTTPResponse.Status +func (r GetV1OrganizationsOrganizationIdDomainsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1OrganizationsOrganizationIdDomainsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PostV1OrganizationsOrganizationIdDomainsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *CustomDomain +} + +// Status returns HTTPResponse.Status +func (r PostV1OrganizationsOrganizationIdDomainsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PostV1OrganizationsOrganizationIdDomainsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DeleteV1OrganizationsOrganizationIdDomainsDomainResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r DeleteV1OrganizationsOrganizationIdDomainsDomainResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteV1OrganizationsOrganizationIdDomainsDomainResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PostV1OrganizationsOrganizationIdDomainsDomainVerifyResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *CustomDomain +} + +// Status returns HTTPResponse.Status +func (r PostV1OrganizationsOrganizationIdDomainsDomainVerifyResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PostV1OrganizationsOrganizationIdDomainsDomainVerifyResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1ProjectsProjectIdBuildsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ListBuildsResponse +} + +// Status returns HTTPResponse.Status +func (r GetV1ProjectsProjectIdBuildsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1ProjectsProjectIdBuildsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PostV1ProjectsProjectIdBuildsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *Build +} + +// Status returns HTTPResponse.Status +func (r PostV1ProjectsProjectIdBuildsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PostV1ProjectsProjectIdBuildsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ListDeploysResponse +} + +// Status returns HTTPResponse.Status +func (r GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *Deploy +} + +// Status returns HTTPResponse.Status +func (r PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GetV1BuildsBuildIdWithResponse request returning *GetV1BuildsBuildIdResponse +func (c *ClientWithResponses) GetV1BuildsBuildIdWithResponse(ctx context.Context, buildId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1BuildsBuildIdResponse, error) { + rsp, err := c.GetV1BuildsBuildId(ctx, buildId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1BuildsBuildIdResponse(rsp) +} + +// DeleteV1DeploysDeployIdWithResponse request returning *DeleteV1DeploysDeployIdResponse +func (c *ClientWithResponses) DeleteV1DeploysDeployIdWithResponse(ctx context.Context, deployId openapi_types.UUID, reqEditors ...RequestEditorFn) (*DeleteV1DeploysDeployIdResponse, error) { + rsp, err := c.DeleteV1DeploysDeployId(ctx, deployId, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteV1DeploysDeployIdResponse(rsp) +} + +// GetV1DeploysDeployIdWithResponse request returning *GetV1DeploysDeployIdResponse +func (c *ClientWithResponses) GetV1DeploysDeployIdWithResponse(ctx context.Context, deployId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1DeploysDeployIdResponse, error) { + rsp, err := c.GetV1DeploysDeployId(ctx, deployId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1DeploysDeployIdResponse(rsp) +} + +// PutV1DeploysDeployIdWithBodyWithResponse request with arbitrary body returning *PutV1DeploysDeployIdResponse +func (c *ClientWithResponses) PutV1DeploysDeployIdWithBodyWithResponse(ctx context.Context, deployId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PutV1DeploysDeployIdResponse, error) { + rsp, err := c.PutV1DeploysDeployIdWithBody(ctx, deployId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePutV1DeploysDeployIdResponse(rsp) +} + +func (c *ClientWithResponses) PutV1DeploysDeployIdWithResponse(ctx context.Context, deployId openapi_types.UUID, body PutV1DeploysDeployIdJSONRequestBody, reqEditors ...RequestEditorFn) (*PutV1DeploysDeployIdResponse, error) { + rsp, err := c.PutV1DeploysDeployId(ctx, deployId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePutV1DeploysDeployIdResponse(rsp) +} + +// GetV1DeploysDeployIdRoutesWithResponse request returning *GetV1DeploysDeployIdRoutesResponse +func (c *ClientWithResponses) GetV1DeploysDeployIdRoutesWithResponse(ctx context.Context, deployId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1DeploysDeployIdRoutesResponse, error) { + rsp, err := c.GetV1DeploysDeployIdRoutes(ctx, deployId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1DeploysDeployIdRoutesResponse(rsp) +} + +// PostV1DeploysDeployIdRoutesWithBodyWithResponse request with arbitrary body returning *PostV1DeploysDeployIdRoutesResponse +func (c *ClientWithResponses) PostV1DeploysDeployIdRoutesWithBodyWithResponse(ctx context.Context, deployId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostV1DeploysDeployIdRoutesResponse, error) { + rsp, err := c.PostV1DeploysDeployIdRoutesWithBody(ctx, deployId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostV1DeploysDeployIdRoutesResponse(rsp) +} + +func (c *ClientWithResponses) PostV1DeploysDeployIdRoutesWithResponse(ctx context.Context, deployId openapi_types.UUID, body PostV1DeploysDeployIdRoutesJSONRequestBody, reqEditors ...RequestEditorFn) (*PostV1DeploysDeployIdRoutesResponse, error) { + rsp, err := c.PostV1DeploysDeployIdRoutes(ctx, deployId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostV1DeploysDeployIdRoutesResponse(rsp) +} + +// DeleteV1DeploysDeployIdRoutesRouteIdWithResponse request returning *DeleteV1DeploysDeployIdRoutesRouteIdResponse +func (c *ClientWithResponses) DeleteV1DeploysDeployIdRoutesRouteIdWithResponse(ctx context.Context, deployId openapi_types.UUID, routeId openapi_types.UUID, reqEditors ...RequestEditorFn) (*DeleteV1DeploysDeployIdRoutesRouteIdResponse, error) { + rsp, err := c.DeleteV1DeploysDeployIdRoutesRouteId(ctx, deployId, routeId, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteV1DeploysDeployIdRoutesRouteIdResponse(rsp) +} + +// GetV1OrganizationsOrganizationIdDomainsWithResponse request returning *GetV1OrganizationsOrganizationIdDomainsResponse +func (c *ClientWithResponses) GetV1OrganizationsOrganizationIdDomainsWithResponse(ctx context.Context, organizationId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1OrganizationsOrganizationIdDomainsResponse, error) { + rsp, err := c.GetV1OrganizationsOrganizationIdDomains(ctx, organizationId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1OrganizationsOrganizationIdDomainsResponse(rsp) +} + +// PostV1OrganizationsOrganizationIdDomainsWithBodyWithResponse request with arbitrary body returning *PostV1OrganizationsOrganizationIdDomainsResponse +func (c *ClientWithResponses) PostV1OrganizationsOrganizationIdDomainsWithBodyWithResponse(ctx context.Context, organizationId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostV1OrganizationsOrganizationIdDomainsResponse, error) { + rsp, err := c.PostV1OrganizationsOrganizationIdDomainsWithBody(ctx, organizationId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostV1OrganizationsOrganizationIdDomainsResponse(rsp) +} + +func (c *ClientWithResponses) PostV1OrganizationsOrganizationIdDomainsWithResponse(ctx context.Context, organizationId openapi_types.UUID, body PostV1OrganizationsOrganizationIdDomainsJSONRequestBody, reqEditors ...RequestEditorFn) (*PostV1OrganizationsOrganizationIdDomainsResponse, error) { + rsp, err := c.PostV1OrganizationsOrganizationIdDomains(ctx, organizationId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostV1OrganizationsOrganizationIdDomainsResponse(rsp) +} + +// DeleteV1OrganizationsOrganizationIdDomainsDomainWithResponse request returning *DeleteV1OrganizationsOrganizationIdDomainsDomainResponse +func (c *ClientWithResponses) DeleteV1OrganizationsOrganizationIdDomainsDomainWithResponse(ctx context.Context, organizationId openapi_types.UUID, domain string, reqEditors ...RequestEditorFn) (*DeleteV1OrganizationsOrganizationIdDomainsDomainResponse, error) { + rsp, err := c.DeleteV1OrganizationsOrganizationIdDomainsDomain(ctx, organizationId, domain, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteV1OrganizationsOrganizationIdDomainsDomainResponse(rsp) +} + +// PostV1OrganizationsOrganizationIdDomainsDomainVerifyWithResponse request returning *PostV1OrganizationsOrganizationIdDomainsDomainVerifyResponse +func (c *ClientWithResponses) PostV1OrganizationsOrganizationIdDomainsDomainVerifyWithResponse(ctx context.Context, organizationId openapi_types.UUID, domain string, reqEditors ...RequestEditorFn) (*PostV1OrganizationsOrganizationIdDomainsDomainVerifyResponse, error) { + rsp, err := c.PostV1OrganizationsOrganizationIdDomainsDomainVerify(ctx, organizationId, domain, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostV1OrganizationsOrganizationIdDomainsDomainVerifyResponse(rsp) +} + +// GetV1ProjectsProjectIdBuildsWithResponse request returning *GetV1ProjectsProjectIdBuildsResponse +func (c *ClientWithResponses) GetV1ProjectsProjectIdBuildsWithResponse(ctx context.Context, projectId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1ProjectsProjectIdBuildsResponse, error) { + rsp, err := c.GetV1ProjectsProjectIdBuilds(ctx, projectId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1ProjectsProjectIdBuildsResponse(rsp) +} + +// PostV1ProjectsProjectIdBuildsWithBodyWithResponse request with arbitrary body returning *PostV1ProjectsProjectIdBuildsResponse +func (c *ClientWithResponses) PostV1ProjectsProjectIdBuildsWithBodyWithResponse(ctx context.Context, projectId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostV1ProjectsProjectIdBuildsResponse, error) { + rsp, err := c.PostV1ProjectsProjectIdBuildsWithBody(ctx, projectId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostV1ProjectsProjectIdBuildsResponse(rsp) +} + +func (c *ClientWithResponses) PostV1ProjectsProjectIdBuildsWithResponse(ctx context.Context, projectId openapi_types.UUID, body PostV1ProjectsProjectIdBuildsJSONRequestBody, reqEditors ...RequestEditorFn) (*PostV1ProjectsProjectIdBuildsResponse, error) { + rsp, err := c.PostV1ProjectsProjectIdBuilds(ctx, projectId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostV1ProjectsProjectIdBuildsResponse(rsp) +} + +// GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithResponse request returning *GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse +func (c *ClientWithResponses) GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithResponse(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse, error) { + rsp, err := c.GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys(ctx, projectId, environmentId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse(rsp) +} + +// PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithBodyWithResponse request with arbitrary body returning *PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse +func (c *ClientWithResponses) PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithBodyWithResponse(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse, error) { + rsp, err := c.PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithBody(ctx, projectId, environmentId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse(rsp) +} + +func (c *ClientWithResponses) PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithResponse(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, body PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysJSONRequestBody, reqEditors ...RequestEditorFn) (*PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse, error) { + rsp, err := c.PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys(ctx, projectId, environmentId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse(rsp) +} + +// ParseGetV1BuildsBuildIdResponse parses an HTTP response from a GetV1BuildsBuildIdWithResponse call +func ParseGetV1BuildsBuildIdResponse(rsp *http.Response) (*GetV1BuildsBuildIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1BuildsBuildIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Build + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseDeleteV1DeploysDeployIdResponse parses an HTTP response from a DeleteV1DeploysDeployIdWithResponse call +func ParseDeleteV1DeploysDeployIdResponse(rsp *http.Response) (*DeleteV1DeploysDeployIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteV1DeploysDeployIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseGetV1DeploysDeployIdResponse parses an HTTP response from a GetV1DeploysDeployIdWithResponse call +func ParseGetV1DeploysDeployIdResponse(rsp *http.Response) (*GetV1DeploysDeployIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1DeploysDeployIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Deploy + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParsePutV1DeploysDeployIdResponse parses an HTTP response from a PutV1DeploysDeployIdWithResponse call +func ParsePutV1DeploysDeployIdResponse(rsp *http.Response) (*PutV1DeploysDeployIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PutV1DeploysDeployIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Deploy + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseGetV1DeploysDeployIdRoutesResponse parses an HTTP response from a GetV1DeploysDeployIdRoutesWithResponse call +func ParseGetV1DeploysDeployIdRoutesResponse(rsp *http.Response) (*GetV1DeploysDeployIdRoutesResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1DeploysDeployIdRoutesResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ListRoutesResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParsePostV1DeploysDeployIdRoutesResponse parses an HTTP response from a PostV1DeploysDeployIdRoutesWithResponse call +func ParsePostV1DeploysDeployIdRoutesResponse(rsp *http.Response) (*PostV1DeploysDeployIdRoutesResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PostV1DeploysDeployIdRoutesResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest Route + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + } + + return response, nil +} + +// ParseDeleteV1DeploysDeployIdRoutesRouteIdResponse parses an HTTP response from a DeleteV1DeploysDeployIdRoutesRouteIdWithResponse call +func ParseDeleteV1DeploysDeployIdRoutesRouteIdResponse(rsp *http.Response) (*DeleteV1DeploysDeployIdRoutesRouteIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteV1DeploysDeployIdRoutesRouteIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseGetV1OrganizationsOrganizationIdDomainsResponse parses an HTTP response from a GetV1OrganizationsOrganizationIdDomainsWithResponse call +func ParseGetV1OrganizationsOrganizationIdDomainsResponse(rsp *http.Response) (*GetV1OrganizationsOrganizationIdDomainsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1OrganizationsOrganizationIdDomainsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ListCustomDomainsResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParsePostV1OrganizationsOrganizationIdDomainsResponse parses an HTTP response from a PostV1OrganizationsOrganizationIdDomainsWithResponse call +func ParsePostV1OrganizationsOrganizationIdDomainsResponse(rsp *http.Response) (*PostV1OrganizationsOrganizationIdDomainsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PostV1OrganizationsOrganizationIdDomainsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest CustomDomain + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + } + + return response, nil +} + +// ParseDeleteV1OrganizationsOrganizationIdDomainsDomainResponse parses an HTTP response from a DeleteV1OrganizationsOrganizationIdDomainsDomainWithResponse call +func ParseDeleteV1OrganizationsOrganizationIdDomainsDomainResponse(rsp *http.Response) (*DeleteV1OrganizationsOrganizationIdDomainsDomainResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteV1OrganizationsOrganizationIdDomainsDomainResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParsePostV1OrganizationsOrganizationIdDomainsDomainVerifyResponse parses an HTTP response from a PostV1OrganizationsOrganizationIdDomainsDomainVerifyWithResponse call +func ParsePostV1OrganizationsOrganizationIdDomainsDomainVerifyResponse(rsp *http.Response) (*PostV1OrganizationsOrganizationIdDomainsDomainVerifyResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PostV1OrganizationsOrganizationIdDomainsDomainVerifyResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest CustomDomain + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseGetV1ProjectsProjectIdBuildsResponse parses an HTTP response from a GetV1ProjectsProjectIdBuildsWithResponse call +func ParseGetV1ProjectsProjectIdBuildsResponse(rsp *http.Response) (*GetV1ProjectsProjectIdBuildsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1ProjectsProjectIdBuildsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ListBuildsResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParsePostV1ProjectsProjectIdBuildsResponse parses an HTTP response from a PostV1ProjectsProjectIdBuildsWithResponse call +func ParsePostV1ProjectsProjectIdBuildsResponse(rsp *http.Response) (*PostV1ProjectsProjectIdBuildsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PostV1ProjectsProjectIdBuildsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest Build + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + } + + return response, nil +} + +// ParseGetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse parses an HTTP response from a GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithResponse call +func ParseGetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse(rsp *http.Response) (*GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ListDeploysResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParsePostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse parses an HTTP response from a PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithResponse call +func ParsePostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse(rsp *http.Response) (*PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest Deploy + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + } + + return response, nil +} diff --git a/internal/api/gen/deploys/generate.go b/internal/api/gen/deploys/generate.go new file mode 100644 index 0000000..32fac44 --- /dev/null +++ b/internal/api/gen/deploys/generate.go @@ -0,0 +1,5 @@ +// Package deploys is the generated client for the deploys API. Never edit +// deploys.gen.go by hand: change internal/api/specs and run `make generate`. +package deploys + +//go:generate go tool oapi-codegen -config cfg.yaml ../../specs/deploys.yaml diff --git a/internal/api/gen/logs/cfg.yaml b/internal/api/gen/logs/cfg.yaml new file mode 100644 index 0000000..a9780c7 --- /dev/null +++ b/internal/api/gen/logs/cfg.yaml @@ -0,0 +1,6 @@ +# oapi-codegen configuration; see generate.go. +package: logs +generate: + client: true + models: true +output: logs.gen.go diff --git a/internal/api/gen/logs/generate.go b/internal/api/gen/logs/generate.go new file mode 100644 index 0000000..2f6e9e9 --- /dev/null +++ b/internal/api/gen/logs/generate.go @@ -0,0 +1,5 @@ +// Package logs is the generated client for the logs API. Never edit +// logs.gen.go by hand: change internal/api/specs and run `make generate`. +package logs + +//go:generate go tool oapi-codegen -config cfg.yaml ../../specs/logs.yaml diff --git a/internal/api/gen/logs/logs.gen.go b/internal/api/gen/logs/logs.gen.go new file mode 100644 index 0000000..8b78d71 --- /dev/null +++ b/internal/api/gen/logs/logs.gen.go @@ -0,0 +1,369 @@ +// Package logs provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT. +package logs + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/oapi-codegen/runtime" + openapi_types "github.com/oapi-codegen/runtime/types" +) + +// LogEntry defines model for LogEntry. +type LogEntry struct { + // Line The raw log line. + Line string `json:"line"` + + // Pod The pod the line came from. + Pod *string `json:"pod,omitempty"` + + // Timestamp RFC3339Nano timestamp of the log line. + Timestamp string `json:"timestamp"` +} + +// LogsResponse defines model for LogsResponse. +type LogsResponse struct { + // Cursor RFC3339Nano timestamp to continue a live tail from (latest entry, or the query start when empty). + Cursor string `json:"cursor"` + Entries []LogEntry `json:"entries"` +} + +// GetV1DeploysDeployIdLogsParams defines parameters for GetV1DeploysDeployIdLogs. +type GetV1DeploysDeployIdLogsParams struct { + // Pod Narrow to a single pod name. + Pod *string `form:"pod,omitempty" json:"pod,omitempty"` + + // Since How far back to read (Go duration, e.g. "1h"). Defaults to 1h. + Since *string `form:"since,omitempty" json:"since,omitempty"` + + // Filter Case-insensitive substring the log line must contain. + Filter *string `form:"filter,omitempty" json:"filter,omitempty"` + + // Match Structured field filters applied to the log's JSON fields, each `` where + // op is one of `=`, `!=`, `=~`, `!~` (e.g. `status=running`, `service=~api-.*`). Repeatable; + // all must match. Only JSON log lines can match a field filter. + Match *[]string `form:"match,omitempty" json:"match,omitempty"` + + // Limit Max number of lines (default 500, max 5000). + Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` +} + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // GetV1DeploysDeployIdLogs request + GetV1DeploysDeployIdLogs(ctx context.Context, deployId openapi_types.UUID, params *GetV1DeploysDeployIdLogsParams, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) GetV1DeploysDeployIdLogs(ctx context.Context, deployId openapi_types.UUID, params *GetV1DeploysDeployIdLogsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1DeploysDeployIdLogsRequest(c.Server, deployId, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewGetV1DeploysDeployIdLogsRequest generates requests for GetV1DeploysDeployIdLogs +func NewGetV1DeploysDeployIdLogsRequest(server string, deployId openapi_types.UUID, params *GetV1DeploysDeployIdLogsParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "deployId", runtime.ParamLocationPath, deployId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/deploys/%s/logs", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Pod != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "pod", runtime.ParamLocationQuery, *params.Pod); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Since != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "since", runtime.ParamLocationQuery, *params.Since); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Filter != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "filter", runtime.ParamLocationQuery, *params.Filter); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Match != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "match", runtime.ParamLocationQuery, *params.Match); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Limit != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // GetV1DeploysDeployIdLogsWithResponse request + GetV1DeploysDeployIdLogsWithResponse(ctx context.Context, deployId openapi_types.UUID, params *GetV1DeploysDeployIdLogsParams, reqEditors ...RequestEditorFn) (*GetV1DeploysDeployIdLogsResponse, error) +} + +type GetV1DeploysDeployIdLogsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *LogsResponse +} + +// Status returns HTTPResponse.Status +func (r GetV1DeploysDeployIdLogsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1DeploysDeployIdLogsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GetV1DeploysDeployIdLogsWithResponse request returning *GetV1DeploysDeployIdLogsResponse +func (c *ClientWithResponses) GetV1DeploysDeployIdLogsWithResponse(ctx context.Context, deployId openapi_types.UUID, params *GetV1DeploysDeployIdLogsParams, reqEditors ...RequestEditorFn) (*GetV1DeploysDeployIdLogsResponse, error) { + rsp, err := c.GetV1DeploysDeployIdLogs(ctx, deployId, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1DeploysDeployIdLogsResponse(rsp) +} + +// ParseGetV1DeploysDeployIdLogsResponse parses an HTTP response from a GetV1DeploysDeployIdLogsWithResponse call +func ParseGetV1DeploysDeployIdLogsResponse(rsp *http.Response) (*GetV1DeploysDeployIdLogsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1DeploysDeployIdLogsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest LogsResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} diff --git a/internal/api/gen/metrics/cfg.yaml b/internal/api/gen/metrics/cfg.yaml new file mode 100644 index 0000000..4096489 --- /dev/null +++ b/internal/api/gen/metrics/cfg.yaml @@ -0,0 +1,6 @@ +# oapi-codegen configuration; see generate.go. +package: metrics +generate: + client: true + models: true +output: metrics.gen.go diff --git a/internal/api/gen/metrics/generate.go b/internal/api/gen/metrics/generate.go new file mode 100644 index 0000000..a5b2938 --- /dev/null +++ b/internal/api/gen/metrics/generate.go @@ -0,0 +1,5 @@ +// Package metrics is the generated client for the metrics API. Never edit +// metrics.gen.go by hand: change internal/api/specs and run `make generate`. +package metrics + +//go:generate go tool oapi-codegen -config cfg.yaml ../../specs/metrics.yaml diff --git a/internal/api/gen/metrics/metrics.gen.go b/internal/api/gen/metrics/metrics.gen.go new file mode 100644 index 0000000..b3c565d --- /dev/null +++ b/internal/api/gen/metrics/metrics.gen.go @@ -0,0 +1,625 @@ +// Package metrics provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT. +package metrics + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/oapi-codegen/runtime" + openapi_types "github.com/oapi-codegen/runtime/types" +) + +// MetricPoint defines model for MetricPoint. +type MetricPoint struct { + // Timestamp RFC3339 timestamp of the sample. + Timestamp string `json:"timestamp"` + Value float64 `json:"value"` +} + +// MetricSeries defines model for MetricSeries. +type MetricSeries struct { + // Labels The series' label set (e.g. pod, deploy_id). + Labels map[string]string `json:"labels"` + Points []MetricPoint `json:"points"` +} + +// MetricTypesResponse defines model for MetricTypesResponse. +type MetricTypesResponse struct { + // Metrics Supported curated metric keys. + Metrics []string `json:"metrics"` +} + +// MetricsResponse defines model for MetricsResponse. +type MetricsResponse struct { + // Metric The curated metric key that was queried. + Metric string `json:"metric"` + Series []MetricSeries `json:"series"` +} + +// GetV1DeploysDeployIdMetricsParams defines parameters for GetV1DeploysDeployIdMetrics. +type GetV1DeploysDeployIdMetricsParams struct { + // Metric Curated metric key (see GET /v1/metric-types). + Metric string `form:"metric" json:"metric"` + + // Pod Narrow to a single pod name. + Pod *string `form:"pod,omitempty" json:"pod,omitempty"` + + // Range How far back to read (Go duration, e.g. "1h"). Defaults to 1h. + Range *string `form:"range,omitempty" json:"range,omitempty"` + + // Step Resolution between points (Go duration, e.g. "30s"). Defaults to ~120 points over the range. + Step *string `form:"step,omitempty" json:"step,omitempty"` +} + +// GetV1PodsPodIdMetricsParams defines parameters for GetV1PodsPodIdMetrics. +type GetV1PodsPodIdMetricsParams struct { + // Metric Curated metric key (see GET /v1/metric-types). + Metric string `form:"metric" json:"metric"` + + // Range How far back to read (Go duration, e.g. "1h"). Defaults to 1h. + Range *string `form:"range,omitempty" json:"range,omitempty"` + + // Step Resolution between points (Go duration, e.g. "30s"). Defaults to ~120 points over the range. + Step *string `form:"step,omitempty" json:"step,omitempty"` +} + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // GetV1DeploysDeployIdMetrics request + GetV1DeploysDeployIdMetrics(ctx context.Context, deployId openapi_types.UUID, params *GetV1DeploysDeployIdMetricsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1MetricTypes request + GetV1MetricTypes(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1PodsPodIdMetrics request + GetV1PodsPodIdMetrics(ctx context.Context, podId string, params *GetV1PodsPodIdMetricsParams, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) GetV1DeploysDeployIdMetrics(ctx context.Context, deployId openapi_types.UUID, params *GetV1DeploysDeployIdMetricsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1DeploysDeployIdMetricsRequest(c.Server, deployId, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1MetricTypes(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1MetricTypesRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1PodsPodIdMetrics(ctx context.Context, podId string, params *GetV1PodsPodIdMetricsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1PodsPodIdMetricsRequest(c.Server, podId, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewGetV1DeploysDeployIdMetricsRequest generates requests for GetV1DeploysDeployIdMetrics +func NewGetV1DeploysDeployIdMetricsRequest(server string, deployId openapi_types.UUID, params *GetV1DeploysDeployIdMetricsParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "deployId", runtime.ParamLocationPath, deployId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/deploys/%s/metrics", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "metric", runtime.ParamLocationQuery, params.Metric); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + if params.Pod != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "pod", runtime.ParamLocationQuery, *params.Pod); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Range != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "range", runtime.ParamLocationQuery, *params.Range); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Step != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "step", runtime.ParamLocationQuery, *params.Step); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetV1MetricTypesRequest generates requests for GetV1MetricTypes +func NewGetV1MetricTypesRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/metric-types") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetV1PodsPodIdMetricsRequest generates requests for GetV1PodsPodIdMetrics +func NewGetV1PodsPodIdMetricsRequest(server string, podId string, params *GetV1PodsPodIdMetricsParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "podId", runtime.ParamLocationPath, podId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/pods/%s/metrics", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "metric", runtime.ParamLocationQuery, params.Metric); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + if params.Range != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "range", runtime.ParamLocationQuery, *params.Range); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Step != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "step", runtime.ParamLocationQuery, *params.Step); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // GetV1DeploysDeployIdMetricsWithResponse request + GetV1DeploysDeployIdMetricsWithResponse(ctx context.Context, deployId openapi_types.UUID, params *GetV1DeploysDeployIdMetricsParams, reqEditors ...RequestEditorFn) (*GetV1DeploysDeployIdMetricsResponse, error) + + // GetV1MetricTypesWithResponse request + GetV1MetricTypesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetV1MetricTypesResponse, error) + + // GetV1PodsPodIdMetricsWithResponse request + GetV1PodsPodIdMetricsWithResponse(ctx context.Context, podId string, params *GetV1PodsPodIdMetricsParams, reqEditors ...RequestEditorFn) (*GetV1PodsPodIdMetricsResponse, error) +} + +type GetV1DeploysDeployIdMetricsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *MetricsResponse +} + +// Status returns HTTPResponse.Status +func (r GetV1DeploysDeployIdMetricsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1DeploysDeployIdMetricsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1MetricTypesResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *MetricTypesResponse +} + +// Status returns HTTPResponse.Status +func (r GetV1MetricTypesResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1MetricTypesResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1PodsPodIdMetricsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *MetricsResponse +} + +// Status returns HTTPResponse.Status +func (r GetV1PodsPodIdMetricsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1PodsPodIdMetricsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GetV1DeploysDeployIdMetricsWithResponse request returning *GetV1DeploysDeployIdMetricsResponse +func (c *ClientWithResponses) GetV1DeploysDeployIdMetricsWithResponse(ctx context.Context, deployId openapi_types.UUID, params *GetV1DeploysDeployIdMetricsParams, reqEditors ...RequestEditorFn) (*GetV1DeploysDeployIdMetricsResponse, error) { + rsp, err := c.GetV1DeploysDeployIdMetrics(ctx, deployId, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1DeploysDeployIdMetricsResponse(rsp) +} + +// GetV1MetricTypesWithResponse request returning *GetV1MetricTypesResponse +func (c *ClientWithResponses) GetV1MetricTypesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetV1MetricTypesResponse, error) { + rsp, err := c.GetV1MetricTypes(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1MetricTypesResponse(rsp) +} + +// GetV1PodsPodIdMetricsWithResponse request returning *GetV1PodsPodIdMetricsResponse +func (c *ClientWithResponses) GetV1PodsPodIdMetricsWithResponse(ctx context.Context, podId string, params *GetV1PodsPodIdMetricsParams, reqEditors ...RequestEditorFn) (*GetV1PodsPodIdMetricsResponse, error) { + rsp, err := c.GetV1PodsPodIdMetrics(ctx, podId, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1PodsPodIdMetricsResponse(rsp) +} + +// ParseGetV1DeploysDeployIdMetricsResponse parses an HTTP response from a GetV1DeploysDeployIdMetricsWithResponse call +func ParseGetV1DeploysDeployIdMetricsResponse(rsp *http.Response) (*GetV1DeploysDeployIdMetricsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1DeploysDeployIdMetricsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest MetricsResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseGetV1MetricTypesResponse parses an HTTP response from a GetV1MetricTypesWithResponse call +func ParseGetV1MetricTypesResponse(rsp *http.Response) (*GetV1MetricTypesResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1MetricTypesResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest MetricTypesResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseGetV1PodsPodIdMetricsResponse parses an HTTP response from a GetV1PodsPodIdMetricsWithResponse call +func ParseGetV1PodsPodIdMetricsResponse(rsp *http.Response) (*GetV1PodsPodIdMetricsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1PodsPodIdMetricsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest MetricsResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} diff --git a/internal/api/gen/org/cfg.yaml b/internal/api/gen/org/cfg.yaml new file mode 100644 index 0000000..1041f51 --- /dev/null +++ b/internal/api/gen/org/cfg.yaml @@ -0,0 +1,6 @@ +# oapi-codegen configuration; see generate.go. +package: org +generate: + client: true + models: true +output: org.gen.go diff --git a/internal/api/gen/org/generate.go b/internal/api/gen/org/generate.go new file mode 100644 index 0000000..3557d72 --- /dev/null +++ b/internal/api/gen/org/generate.go @@ -0,0 +1,5 @@ +// Package org is the generated client for the org API. Never edit +// org.gen.go by hand: change internal/api/specs and run `make generate`. +package org + +//go:generate go tool oapi-codegen -config cfg.yaml ../../specs/org.yaml diff --git a/internal/api/gen/org/org.gen.go b/internal/api/gen/org/org.gen.go new file mode 100644 index 0000000..956b701 --- /dev/null +++ b/internal/api/gen/org/org.gen.go @@ -0,0 +1,2064 @@ +// Package org provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT. +package org + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/oapi-codegen/runtime" + openapi_types "github.com/oapi-codegen/runtime/types" +) + +// Defines values for EnvironmentTenancyType. +const ( + DEDICATED EnvironmentTenancyType = "DEDICATED" + SHARED EnvironmentTenancyType = "SHARED" +) + +// Environment defines model for Environment. +type Environment struct { + // CreatedAt Timestamp when the environment was created + CreatedAt time.Time `json:"created_at"` + + // Id UUID of the environment + Id openapi_types.UUID `json:"id"` + + // InstanceType Dedicated node instance type (present when set); required for DEDICATED environments. + InstanceType *string `json:"instance_type,omitempty"` + + // Name Name of the environment + Name string `json:"name"` + + // ProjectId UUID of the parent project + ProjectId openapi_types.UUID `json:"project_id"` + + // TenancyType Tenancy policy for the environment. SHARED places inheriting deploys on the global shared pool; DEDICATED leases nodes to the environment so its deploys co-tenant on environment-leased nodes (a deploy can still override to its own dedicated nodes). + TenancyType EnvironmentTenancyType `json:"tenancy_type"` + + // UpdatedAt Timestamp when the environment was last updated + UpdatedAt time.Time `json:"updated_at"` +} + +// EnvironmentSecret defines model for EnvironmentSecret. +type EnvironmentSecret struct { + // Key The name/key of the secret. The value is never returned. + Key string `json:"key"` +} + +// EnvironmentTenancyType Tenancy policy for the environment. SHARED places inheriting deploys on the global shared pool; DEDICATED leases nodes to the environment so its deploys co-tenant on environment-leased nodes (a deploy can still override to its own dedicated nodes). +type EnvironmentTenancyType string + +// ListEnvironmentSecretsResponse defines model for ListEnvironmentSecretsResponse. +type ListEnvironmentSecretsResponse struct { + // Limit Maximum number of secrets returned. + Limit int32 `json:"limit"` + + // Offset Offset of the first secret returned. + Offset int32 `json:"offset"` + Secrets []EnvironmentSecret `json:"secrets"` + + // Total Total number of secrets in the environment. + Total int64 `json:"total"` +} + +// ListEnvironmentsResponse defines model for ListEnvironmentsResponse. +type ListEnvironmentsResponse struct { + Environments []Environment `json:"environments"` +} + +// ListLocationsResponse defines model for ListLocationsResponse. +type ListLocationsResponse struct { + Locations []Location `json:"locations"` +} + +// ListOrganizationsResponse defines model for ListOrganizationsResponse. +type ListOrganizationsResponse struct { + Organizations []Organization `json:"organizations"` +} + +// ListPodsResponse defines model for ListPodsResponse. +type ListPodsResponse struct { + // Limit Maximum number of pods returned. + Limit int32 `json:"limit"` + + // Offset Offset of the first pod returned. + Offset int32 `json:"offset"` + Results []Pod `json:"results"` + + // Total Total number of pods matching the query. + Total int64 `json:"total"` +} + +// ListProjectsResponse defines model for ListProjectsResponse. +type ListProjectsResponse struct { + Projects []Project `json:"projects"` +} + +// Location defines model for Location. +type Location struct { + // Id The location identifier (region name). + Id string `json:"id"` +} + +// Organization defines model for Organization. +type Organization struct { + // CreatedAt Timestamp when the organization was created + CreatedAt time.Time `json:"created_at"` + + // Id UUID of the organization + Id openapi_types.UUID `json:"id"` + + // Name Display name of the organization. Free-form text; not unique. + Name string `json:"name"` + + // Slug Unique, immutable, URL-safe identifier of the organization (3-39 characters, lowercase a-z, digits and single hyphens). Usable wherever an organization id is accepted by the CLI. + Slug string `json:"slug"` + + // UpdatedAt Timestamp when the organization was last updated + UpdatedAt time.Time `json:"updated_at"` +} + +// Pod A pod belonging to one of the caller's deploys. +type Pod struct { + // ClusterName The name of the cluster the pod is in. + ClusterName string `json:"cluster_name"` + + // DeployId The deploy the pod belongs to. + DeployId string `json:"deploy_id"` + + // EnvironmentId The environment the pod belongs to. + EnvironmentId string `json:"environment_id"` + + // Id The ID of the pod. + Id string `json:"id"` + + // LocationId The location ID of the pod. + LocationId string `json:"location_id"` + + // Name The name of the pod. + Name string `json:"name"` + + // Phase The phase the pod is in. + Phase string `json:"phase"` + + // PhaseReason The reason for the pod being in its current phase. + PhaseReason *string `json:"phase_reason,omitempty"` + + // ProjectId The project the pod belongs to. + ProjectId string `json:"project_id"` +} + +// Project defines model for Project. +type Project struct { + // CreatedAt Timestamp when the project was created + CreatedAt time.Time `json:"created_at"` + + // Id UUID of the project + Id openapi_types.UUID `json:"id"` + + // Name Name of the project + Name string `json:"name"` + + // OrganizationId UUID of the parent organization + OrganizationId openapi_types.UUID `json:"organization_id"` + + // UpdatedAt Timestamp when the project was last updated + UpdatedAt time.Time `json:"updated_at"` +} + +// Limit defines model for Limit. +type Limit = int32 + +// Offset defines model for Offset. +type Offset = int32 + +// CreateEnvironmentSecretRequest defines model for CreateEnvironmentSecretRequest. +type CreateEnvironmentSecretRequest struct { + // Value The secret value. + Value string `json:"value"` +} + +// GetV1DeploysDeployIdPodsParams defines parameters for GetV1DeploysDeployIdPods. +type GetV1DeploysDeployIdPodsParams struct { + Limit *Limit `form:"limit,omitempty" json:"limit,omitempty"` + Offset *Offset `form:"offset,omitempty" json:"offset,omitempty"` +} + +// GetV1EnvironmentsEnvironmentIdPodsParams defines parameters for GetV1EnvironmentsEnvironmentIdPods. +type GetV1EnvironmentsEnvironmentIdPodsParams struct { + Limit *Limit `form:"limit,omitempty" json:"limit,omitempty"` + Offset *Offset `form:"offset,omitempty" json:"offset,omitempty"` +} + +// ListEnvironmentSecretsV1Params defines parameters for ListEnvironmentSecretsV1. +type ListEnvironmentSecretsV1Params struct { + Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` + Offset *int32 `form:"offset,omitempty" json:"offset,omitempty"` +} + +// CreateEnvironmentSecretV1JSONBody defines parameters for CreateEnvironmentSecretV1. +type CreateEnvironmentSecretV1JSONBody struct { + // Value The secret value. + Value string `json:"value"` +} + +// GetV1ProjectsProjectIdPodsParams defines parameters for GetV1ProjectsProjectIdPods. +type GetV1ProjectsProjectIdPodsParams struct { + Limit *Limit `form:"limit,omitempty" json:"limit,omitempty"` + Offset *Offset `form:"offset,omitempty" json:"offset,omitempty"` +} + +// CreateEnvironmentSecretV1JSONRequestBody defines body for CreateEnvironmentSecretV1 for application/json ContentType. +type CreateEnvironmentSecretV1JSONRequestBody CreateEnvironmentSecretV1JSONBody + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // GetV1DeploysDeployIdPods request + GetV1DeploysDeployIdPods(ctx context.Context, deployId openapi_types.UUID, params *GetV1DeploysDeployIdPodsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1EnvironmentsEnvironmentId request + GetV1EnvironmentsEnvironmentId(ctx context.Context, environmentId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1EnvironmentsEnvironmentIdPods request + GetV1EnvironmentsEnvironmentIdPods(ctx context.Context, environmentId openapi_types.UUID, params *GetV1EnvironmentsEnvironmentIdPodsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1Locations request + GetV1Locations(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1Organizations request + GetV1Organizations(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1OrganizationsOrganizationId request + GetV1OrganizationsOrganizationId(ctx context.Context, organizationId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1OrganizationsOrganizationIdProjects request + GetV1OrganizationsOrganizationIdProjects(ctx context.Context, organizationId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1PodsPodId request + GetV1PodsPodId(ctx context.Context, podId string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1ProjectsProjectId request + GetV1ProjectsProjectId(ctx context.Context, projectId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1ProjectsProjectIdEnvironments request + GetV1ProjectsProjectIdEnvironments(ctx context.Context, projectId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + + // ListEnvironmentSecretsV1 request + ListEnvironmentSecretsV1(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, params *ListEnvironmentSecretsV1Params, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteEnvironmentSecretV1 request + DeleteEnvironmentSecretV1(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, secretName string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // CreateEnvironmentSecretV1WithBody request with any body + CreateEnvironmentSecretV1WithBody(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, secretName string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + CreateEnvironmentSecretV1(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, secretName string, body CreateEnvironmentSecretV1JSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetV1ProjectsProjectIdPods request + GetV1ProjectsProjectIdPods(ctx context.Context, projectId openapi_types.UUID, params *GetV1ProjectsProjectIdPodsParams, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) GetV1DeploysDeployIdPods(ctx context.Context, deployId openapi_types.UUID, params *GetV1DeploysDeployIdPodsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1DeploysDeployIdPodsRequest(c.Server, deployId, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1EnvironmentsEnvironmentId(ctx context.Context, environmentId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1EnvironmentsEnvironmentIdRequest(c.Server, environmentId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1EnvironmentsEnvironmentIdPods(ctx context.Context, environmentId openapi_types.UUID, params *GetV1EnvironmentsEnvironmentIdPodsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1EnvironmentsEnvironmentIdPodsRequest(c.Server, environmentId, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1Locations(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1LocationsRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1Organizations(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1OrganizationsRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1OrganizationsOrganizationId(ctx context.Context, organizationId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1OrganizationsOrganizationIdRequest(c.Server, organizationId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1OrganizationsOrganizationIdProjects(ctx context.Context, organizationId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1OrganizationsOrganizationIdProjectsRequest(c.Server, organizationId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1PodsPodId(ctx context.Context, podId string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1PodsPodIdRequest(c.Server, podId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1ProjectsProjectId(ctx context.Context, projectId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1ProjectsProjectIdRequest(c.Server, projectId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1ProjectsProjectIdEnvironments(ctx context.Context, projectId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1ProjectsProjectIdEnvironmentsRequest(c.Server, projectId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ListEnvironmentSecretsV1(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, params *ListEnvironmentSecretsV1Params, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListEnvironmentSecretsV1Request(c.Server, projectId, environmentId, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DeleteEnvironmentSecretV1(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, secretName string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteEnvironmentSecretV1Request(c.Server, projectId, environmentId, secretName) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreateEnvironmentSecretV1WithBody(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, secretName string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateEnvironmentSecretV1RequestWithBody(c.Server, projectId, environmentId, secretName, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreateEnvironmentSecretV1(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, secretName string, body CreateEnvironmentSecretV1JSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateEnvironmentSecretV1Request(c.Server, projectId, environmentId, secretName, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetV1ProjectsProjectIdPods(ctx context.Context, projectId openapi_types.UUID, params *GetV1ProjectsProjectIdPodsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV1ProjectsProjectIdPodsRequest(c.Server, projectId, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewGetV1DeploysDeployIdPodsRequest generates requests for GetV1DeploysDeployIdPods +func NewGetV1DeploysDeployIdPodsRequest(server string, deployId openapi_types.UUID, params *GetV1DeploysDeployIdPodsParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "deploy_id", runtime.ParamLocationPath, deployId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/deploys/%s/pods", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Limit != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Offset != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "offset", runtime.ParamLocationQuery, *params.Offset); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetV1EnvironmentsEnvironmentIdRequest generates requests for GetV1EnvironmentsEnvironmentId +func NewGetV1EnvironmentsEnvironmentIdRequest(server string, environmentId openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "environment_id", runtime.ParamLocationPath, environmentId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/environments/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetV1EnvironmentsEnvironmentIdPodsRequest generates requests for GetV1EnvironmentsEnvironmentIdPods +func NewGetV1EnvironmentsEnvironmentIdPodsRequest(server string, environmentId openapi_types.UUID, params *GetV1EnvironmentsEnvironmentIdPodsParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "environment_id", runtime.ParamLocationPath, environmentId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/environments/%s/pods", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Limit != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Offset != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "offset", runtime.ParamLocationQuery, *params.Offset); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetV1LocationsRequest generates requests for GetV1Locations +func NewGetV1LocationsRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/locations") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetV1OrganizationsRequest generates requests for GetV1Organizations +func NewGetV1OrganizationsRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/organizations") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetV1OrganizationsOrganizationIdRequest generates requests for GetV1OrganizationsOrganizationId +func NewGetV1OrganizationsOrganizationIdRequest(server string, organizationId openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "organization_id", runtime.ParamLocationPath, organizationId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/organizations/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetV1OrganizationsOrganizationIdProjectsRequest generates requests for GetV1OrganizationsOrganizationIdProjects +func NewGetV1OrganizationsOrganizationIdProjectsRequest(server string, organizationId openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "organization_id", runtime.ParamLocationPath, organizationId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/organizations/%s/projects", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetV1PodsPodIdRequest generates requests for GetV1PodsPodId +func NewGetV1PodsPodIdRequest(server string, podId string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "pod_id", runtime.ParamLocationPath, podId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/pods/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetV1ProjectsProjectIdRequest generates requests for GetV1ProjectsProjectId +func NewGetV1ProjectsProjectIdRequest(server string, projectId openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "project_id", runtime.ParamLocationPath, projectId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetV1ProjectsProjectIdEnvironmentsRequest generates requests for GetV1ProjectsProjectIdEnvironments +func NewGetV1ProjectsProjectIdEnvironmentsRequest(server string, projectId openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "project_id", runtime.ParamLocationPath, projectId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/environments", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewListEnvironmentSecretsV1Request generates requests for ListEnvironmentSecretsV1 +func NewListEnvironmentSecretsV1Request(server string, projectId openapi_types.UUID, environmentId openapi_types.UUID, params *ListEnvironmentSecretsV1Params) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "project_id", runtime.ParamLocationPath, projectId) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "environment_id", runtime.ParamLocationPath, environmentId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/environments/%s/secrets", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Limit != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Offset != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "offset", runtime.ParamLocationQuery, *params.Offset); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewDeleteEnvironmentSecretV1Request generates requests for DeleteEnvironmentSecretV1 +func NewDeleteEnvironmentSecretV1Request(server string, projectId openapi_types.UUID, environmentId openapi_types.UUID, secretName string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "project_id", runtime.ParamLocationPath, projectId) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "environment_id", runtime.ParamLocationPath, environmentId) + if err != nil { + return nil, err + } + + var pathParam2 string + + pathParam2, err = runtime.StyleParamWithLocation("simple", false, "secret_name", runtime.ParamLocationPath, secretName) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/environments/%s/secrets/%s", pathParam0, pathParam1, pathParam2) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewCreateEnvironmentSecretV1Request calls the generic CreateEnvironmentSecretV1 builder with application/json body +func NewCreateEnvironmentSecretV1Request(server string, projectId openapi_types.UUID, environmentId openapi_types.UUID, secretName string, body CreateEnvironmentSecretV1JSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewCreateEnvironmentSecretV1RequestWithBody(server, projectId, environmentId, secretName, "application/json", bodyReader) +} + +// NewCreateEnvironmentSecretV1RequestWithBody generates requests for CreateEnvironmentSecretV1 with any type of body +func NewCreateEnvironmentSecretV1RequestWithBody(server string, projectId openapi_types.UUID, environmentId openapi_types.UUID, secretName string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "project_id", runtime.ParamLocationPath, projectId) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "environment_id", runtime.ParamLocationPath, environmentId) + if err != nil { + return nil, err + } + + var pathParam2 string + + pathParam2, err = runtime.StyleParamWithLocation("simple", false, "secret_name", runtime.ParamLocationPath, secretName) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/environments/%s/secrets/%s", pathParam0, pathParam1, pathParam2) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewGetV1ProjectsProjectIdPodsRequest generates requests for GetV1ProjectsProjectIdPods +func NewGetV1ProjectsProjectIdPodsRequest(server string, projectId openapi_types.UUID, params *GetV1ProjectsProjectIdPodsParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "project_id", runtime.ParamLocationPath, projectId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/pods", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Limit != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Offset != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "offset", runtime.ParamLocationQuery, *params.Offset); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // GetV1DeploysDeployIdPodsWithResponse request + GetV1DeploysDeployIdPodsWithResponse(ctx context.Context, deployId openapi_types.UUID, params *GetV1DeploysDeployIdPodsParams, reqEditors ...RequestEditorFn) (*GetV1DeploysDeployIdPodsResponse, error) + + // GetV1EnvironmentsEnvironmentIdWithResponse request + GetV1EnvironmentsEnvironmentIdWithResponse(ctx context.Context, environmentId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1EnvironmentsEnvironmentIdResponse, error) + + // GetV1EnvironmentsEnvironmentIdPodsWithResponse request + GetV1EnvironmentsEnvironmentIdPodsWithResponse(ctx context.Context, environmentId openapi_types.UUID, params *GetV1EnvironmentsEnvironmentIdPodsParams, reqEditors ...RequestEditorFn) (*GetV1EnvironmentsEnvironmentIdPodsResponse, error) + + // GetV1LocationsWithResponse request + GetV1LocationsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetV1LocationsResponse, error) + + // GetV1OrganizationsWithResponse request + GetV1OrganizationsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetV1OrganizationsResponse, error) + + // GetV1OrganizationsOrganizationIdWithResponse request + GetV1OrganizationsOrganizationIdWithResponse(ctx context.Context, organizationId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1OrganizationsOrganizationIdResponse, error) + + // GetV1OrganizationsOrganizationIdProjectsWithResponse request + GetV1OrganizationsOrganizationIdProjectsWithResponse(ctx context.Context, organizationId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1OrganizationsOrganizationIdProjectsResponse, error) + + // GetV1PodsPodIdWithResponse request + GetV1PodsPodIdWithResponse(ctx context.Context, podId string, reqEditors ...RequestEditorFn) (*GetV1PodsPodIdResponse, error) + + // GetV1ProjectsProjectIdWithResponse request + GetV1ProjectsProjectIdWithResponse(ctx context.Context, projectId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1ProjectsProjectIdResponse, error) + + // GetV1ProjectsProjectIdEnvironmentsWithResponse request + GetV1ProjectsProjectIdEnvironmentsWithResponse(ctx context.Context, projectId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1ProjectsProjectIdEnvironmentsResponse, error) + + // ListEnvironmentSecretsV1WithResponse request + ListEnvironmentSecretsV1WithResponse(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, params *ListEnvironmentSecretsV1Params, reqEditors ...RequestEditorFn) (*ListEnvironmentSecretsV1Response, error) + + // DeleteEnvironmentSecretV1WithResponse request + DeleteEnvironmentSecretV1WithResponse(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, secretName string, reqEditors ...RequestEditorFn) (*DeleteEnvironmentSecretV1Response, error) + + // CreateEnvironmentSecretV1WithBodyWithResponse request with any body + CreateEnvironmentSecretV1WithBodyWithResponse(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, secretName string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateEnvironmentSecretV1Response, error) + + CreateEnvironmentSecretV1WithResponse(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, secretName string, body CreateEnvironmentSecretV1JSONRequestBody, reqEditors ...RequestEditorFn) (*CreateEnvironmentSecretV1Response, error) + + // GetV1ProjectsProjectIdPodsWithResponse request + GetV1ProjectsProjectIdPodsWithResponse(ctx context.Context, projectId openapi_types.UUID, params *GetV1ProjectsProjectIdPodsParams, reqEditors ...RequestEditorFn) (*GetV1ProjectsProjectIdPodsResponse, error) +} + +type GetV1DeploysDeployIdPodsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ListPodsResponse +} + +// Status returns HTTPResponse.Status +func (r GetV1DeploysDeployIdPodsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1DeploysDeployIdPodsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1EnvironmentsEnvironmentIdResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Environment +} + +// Status returns HTTPResponse.Status +func (r GetV1EnvironmentsEnvironmentIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1EnvironmentsEnvironmentIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1EnvironmentsEnvironmentIdPodsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ListPodsResponse +} + +// Status returns HTTPResponse.Status +func (r GetV1EnvironmentsEnvironmentIdPodsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1EnvironmentsEnvironmentIdPodsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1LocationsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ListLocationsResponse +} + +// Status returns HTTPResponse.Status +func (r GetV1LocationsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1LocationsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1OrganizationsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ListOrganizationsResponse +} + +// Status returns HTTPResponse.Status +func (r GetV1OrganizationsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1OrganizationsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1OrganizationsOrganizationIdResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Organization +} + +// Status returns HTTPResponse.Status +func (r GetV1OrganizationsOrganizationIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1OrganizationsOrganizationIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1OrganizationsOrganizationIdProjectsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ListProjectsResponse +} + +// Status returns HTTPResponse.Status +func (r GetV1OrganizationsOrganizationIdProjectsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1OrganizationsOrganizationIdProjectsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1PodsPodIdResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Pod +} + +// Status returns HTTPResponse.Status +func (r GetV1PodsPodIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1PodsPodIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1ProjectsProjectIdResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Project +} + +// Status returns HTTPResponse.Status +func (r GetV1ProjectsProjectIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1ProjectsProjectIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1ProjectsProjectIdEnvironmentsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ListEnvironmentsResponse +} + +// Status returns HTTPResponse.Status +func (r GetV1ProjectsProjectIdEnvironmentsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1ProjectsProjectIdEnvironmentsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type ListEnvironmentSecretsV1Response struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ListEnvironmentSecretsResponse +} + +// Status returns HTTPResponse.Status +func (r ListEnvironmentSecretsV1Response) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ListEnvironmentSecretsV1Response) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DeleteEnvironmentSecretV1Response struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r DeleteEnvironmentSecretV1Response) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteEnvironmentSecretV1Response) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type CreateEnvironmentSecretV1Response struct { + Body []byte + HTTPResponse *http.Response + JSON201 *EnvironmentSecret +} + +// Status returns HTTPResponse.Status +func (r CreateEnvironmentSecretV1Response) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreateEnvironmentSecretV1Response) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetV1ProjectsProjectIdPodsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ListPodsResponse +} + +// Status returns HTTPResponse.Status +func (r GetV1ProjectsProjectIdPodsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV1ProjectsProjectIdPodsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GetV1DeploysDeployIdPodsWithResponse request returning *GetV1DeploysDeployIdPodsResponse +func (c *ClientWithResponses) GetV1DeploysDeployIdPodsWithResponse(ctx context.Context, deployId openapi_types.UUID, params *GetV1DeploysDeployIdPodsParams, reqEditors ...RequestEditorFn) (*GetV1DeploysDeployIdPodsResponse, error) { + rsp, err := c.GetV1DeploysDeployIdPods(ctx, deployId, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1DeploysDeployIdPodsResponse(rsp) +} + +// GetV1EnvironmentsEnvironmentIdWithResponse request returning *GetV1EnvironmentsEnvironmentIdResponse +func (c *ClientWithResponses) GetV1EnvironmentsEnvironmentIdWithResponse(ctx context.Context, environmentId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1EnvironmentsEnvironmentIdResponse, error) { + rsp, err := c.GetV1EnvironmentsEnvironmentId(ctx, environmentId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1EnvironmentsEnvironmentIdResponse(rsp) +} + +// GetV1EnvironmentsEnvironmentIdPodsWithResponse request returning *GetV1EnvironmentsEnvironmentIdPodsResponse +func (c *ClientWithResponses) GetV1EnvironmentsEnvironmentIdPodsWithResponse(ctx context.Context, environmentId openapi_types.UUID, params *GetV1EnvironmentsEnvironmentIdPodsParams, reqEditors ...RequestEditorFn) (*GetV1EnvironmentsEnvironmentIdPodsResponse, error) { + rsp, err := c.GetV1EnvironmentsEnvironmentIdPods(ctx, environmentId, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1EnvironmentsEnvironmentIdPodsResponse(rsp) +} + +// GetV1LocationsWithResponse request returning *GetV1LocationsResponse +func (c *ClientWithResponses) GetV1LocationsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetV1LocationsResponse, error) { + rsp, err := c.GetV1Locations(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1LocationsResponse(rsp) +} + +// GetV1OrganizationsWithResponse request returning *GetV1OrganizationsResponse +func (c *ClientWithResponses) GetV1OrganizationsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetV1OrganizationsResponse, error) { + rsp, err := c.GetV1Organizations(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1OrganizationsResponse(rsp) +} + +// GetV1OrganizationsOrganizationIdWithResponse request returning *GetV1OrganizationsOrganizationIdResponse +func (c *ClientWithResponses) GetV1OrganizationsOrganizationIdWithResponse(ctx context.Context, organizationId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1OrganizationsOrganizationIdResponse, error) { + rsp, err := c.GetV1OrganizationsOrganizationId(ctx, organizationId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1OrganizationsOrganizationIdResponse(rsp) +} + +// GetV1OrganizationsOrganizationIdProjectsWithResponse request returning *GetV1OrganizationsOrganizationIdProjectsResponse +func (c *ClientWithResponses) GetV1OrganizationsOrganizationIdProjectsWithResponse(ctx context.Context, organizationId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1OrganizationsOrganizationIdProjectsResponse, error) { + rsp, err := c.GetV1OrganizationsOrganizationIdProjects(ctx, organizationId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1OrganizationsOrganizationIdProjectsResponse(rsp) +} + +// GetV1PodsPodIdWithResponse request returning *GetV1PodsPodIdResponse +func (c *ClientWithResponses) GetV1PodsPodIdWithResponse(ctx context.Context, podId string, reqEditors ...RequestEditorFn) (*GetV1PodsPodIdResponse, error) { + rsp, err := c.GetV1PodsPodId(ctx, podId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1PodsPodIdResponse(rsp) +} + +// GetV1ProjectsProjectIdWithResponse request returning *GetV1ProjectsProjectIdResponse +func (c *ClientWithResponses) GetV1ProjectsProjectIdWithResponse(ctx context.Context, projectId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1ProjectsProjectIdResponse, error) { + rsp, err := c.GetV1ProjectsProjectId(ctx, projectId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1ProjectsProjectIdResponse(rsp) +} + +// GetV1ProjectsProjectIdEnvironmentsWithResponse request returning *GetV1ProjectsProjectIdEnvironmentsResponse +func (c *ClientWithResponses) GetV1ProjectsProjectIdEnvironmentsWithResponse(ctx context.Context, projectId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetV1ProjectsProjectIdEnvironmentsResponse, error) { + rsp, err := c.GetV1ProjectsProjectIdEnvironments(ctx, projectId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1ProjectsProjectIdEnvironmentsResponse(rsp) +} + +// ListEnvironmentSecretsV1WithResponse request returning *ListEnvironmentSecretsV1Response +func (c *ClientWithResponses) ListEnvironmentSecretsV1WithResponse(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, params *ListEnvironmentSecretsV1Params, reqEditors ...RequestEditorFn) (*ListEnvironmentSecretsV1Response, error) { + rsp, err := c.ListEnvironmentSecretsV1(ctx, projectId, environmentId, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseListEnvironmentSecretsV1Response(rsp) +} + +// DeleteEnvironmentSecretV1WithResponse request returning *DeleteEnvironmentSecretV1Response +func (c *ClientWithResponses) DeleteEnvironmentSecretV1WithResponse(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, secretName string, reqEditors ...RequestEditorFn) (*DeleteEnvironmentSecretV1Response, error) { + rsp, err := c.DeleteEnvironmentSecretV1(ctx, projectId, environmentId, secretName, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteEnvironmentSecretV1Response(rsp) +} + +// CreateEnvironmentSecretV1WithBodyWithResponse request with arbitrary body returning *CreateEnvironmentSecretV1Response +func (c *ClientWithResponses) CreateEnvironmentSecretV1WithBodyWithResponse(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, secretName string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateEnvironmentSecretV1Response, error) { + rsp, err := c.CreateEnvironmentSecretV1WithBody(ctx, projectId, environmentId, secretName, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateEnvironmentSecretV1Response(rsp) +} + +func (c *ClientWithResponses) CreateEnvironmentSecretV1WithResponse(ctx context.Context, projectId openapi_types.UUID, environmentId openapi_types.UUID, secretName string, body CreateEnvironmentSecretV1JSONRequestBody, reqEditors ...RequestEditorFn) (*CreateEnvironmentSecretV1Response, error) { + rsp, err := c.CreateEnvironmentSecretV1(ctx, projectId, environmentId, secretName, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateEnvironmentSecretV1Response(rsp) +} + +// GetV1ProjectsProjectIdPodsWithResponse request returning *GetV1ProjectsProjectIdPodsResponse +func (c *ClientWithResponses) GetV1ProjectsProjectIdPodsWithResponse(ctx context.Context, projectId openapi_types.UUID, params *GetV1ProjectsProjectIdPodsParams, reqEditors ...RequestEditorFn) (*GetV1ProjectsProjectIdPodsResponse, error) { + rsp, err := c.GetV1ProjectsProjectIdPods(ctx, projectId, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV1ProjectsProjectIdPodsResponse(rsp) +} + +// ParseGetV1DeploysDeployIdPodsResponse parses an HTTP response from a GetV1DeploysDeployIdPodsWithResponse call +func ParseGetV1DeploysDeployIdPodsResponse(rsp *http.Response) (*GetV1DeploysDeployIdPodsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1DeploysDeployIdPodsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ListPodsResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseGetV1EnvironmentsEnvironmentIdResponse parses an HTTP response from a GetV1EnvironmentsEnvironmentIdWithResponse call +func ParseGetV1EnvironmentsEnvironmentIdResponse(rsp *http.Response) (*GetV1EnvironmentsEnvironmentIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1EnvironmentsEnvironmentIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Environment + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseGetV1EnvironmentsEnvironmentIdPodsResponse parses an HTTP response from a GetV1EnvironmentsEnvironmentIdPodsWithResponse call +func ParseGetV1EnvironmentsEnvironmentIdPodsResponse(rsp *http.Response) (*GetV1EnvironmentsEnvironmentIdPodsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1EnvironmentsEnvironmentIdPodsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ListPodsResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseGetV1LocationsResponse parses an HTTP response from a GetV1LocationsWithResponse call +func ParseGetV1LocationsResponse(rsp *http.Response) (*GetV1LocationsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1LocationsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ListLocationsResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseGetV1OrganizationsResponse parses an HTTP response from a GetV1OrganizationsWithResponse call +func ParseGetV1OrganizationsResponse(rsp *http.Response) (*GetV1OrganizationsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1OrganizationsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ListOrganizationsResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseGetV1OrganizationsOrganizationIdResponse parses an HTTP response from a GetV1OrganizationsOrganizationIdWithResponse call +func ParseGetV1OrganizationsOrganizationIdResponse(rsp *http.Response) (*GetV1OrganizationsOrganizationIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1OrganizationsOrganizationIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Organization + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseGetV1OrganizationsOrganizationIdProjectsResponse parses an HTTP response from a GetV1OrganizationsOrganizationIdProjectsWithResponse call +func ParseGetV1OrganizationsOrganizationIdProjectsResponse(rsp *http.Response) (*GetV1OrganizationsOrganizationIdProjectsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1OrganizationsOrganizationIdProjectsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ListProjectsResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseGetV1PodsPodIdResponse parses an HTTP response from a GetV1PodsPodIdWithResponse call +func ParseGetV1PodsPodIdResponse(rsp *http.Response) (*GetV1PodsPodIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1PodsPodIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Pod + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseGetV1ProjectsProjectIdResponse parses an HTTP response from a GetV1ProjectsProjectIdWithResponse call +func ParseGetV1ProjectsProjectIdResponse(rsp *http.Response) (*GetV1ProjectsProjectIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1ProjectsProjectIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Project + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseGetV1ProjectsProjectIdEnvironmentsResponse parses an HTTP response from a GetV1ProjectsProjectIdEnvironmentsWithResponse call +func ParseGetV1ProjectsProjectIdEnvironmentsResponse(rsp *http.Response) (*GetV1ProjectsProjectIdEnvironmentsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1ProjectsProjectIdEnvironmentsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ListEnvironmentsResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseListEnvironmentSecretsV1Response parses an HTTP response from a ListEnvironmentSecretsV1WithResponse call +func ParseListEnvironmentSecretsV1Response(rsp *http.Response) (*ListEnvironmentSecretsV1Response, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ListEnvironmentSecretsV1Response{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ListEnvironmentSecretsResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseDeleteEnvironmentSecretV1Response parses an HTTP response from a DeleteEnvironmentSecretV1WithResponse call +func ParseDeleteEnvironmentSecretV1Response(rsp *http.Response) (*DeleteEnvironmentSecretV1Response, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteEnvironmentSecretV1Response{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseCreateEnvironmentSecretV1Response parses an HTTP response from a CreateEnvironmentSecretV1WithResponse call +func ParseCreateEnvironmentSecretV1Response(rsp *http.Response) (*CreateEnvironmentSecretV1Response, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &CreateEnvironmentSecretV1Response{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest EnvironmentSecret + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + } + + return response, nil +} + +// ParseGetV1ProjectsProjectIdPodsResponse parses an HTTP response from a GetV1ProjectsProjectIdPodsWithResponse call +func ParseGetV1ProjectsProjectIdPodsResponse(rsp *http.Response) (*GetV1ProjectsProjectIdPodsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV1ProjectsProjectIdPodsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ListPodsResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} diff --git a/internal/api/specs/allowlist.yaml b/internal/api/specs/allowlist.yaml new file mode 100644 index 0000000..bdd559d --- /dev/null +++ b/internal/api/specs/allowlist.yaml @@ -0,0 +1,112 @@ +# What the CLI may vendor from the platform's private OpenAPI specs. +# +# This repository is public. hack/specprune publishes only the operations +# listed here and the components they reference, and refuses to write +# anything while a description matches deny_patterns or a property name +# matches suspect_fields without being dropped or reviewed. Every change to +# this file needs review: it decides what becomes public. +source: + repo: devlyft/devlyft + ref: bef84e3b430ac77c34dab97cd1c94d976cbdb49d + +deny_patterns: + - (?i)spiffe|svid + # "Internal server error" is fine; naming internal services or hops is not. + - (?i)\binternal\s+(?:[a-z]+-api|service|network|hosts?|only|endpoint|identity)|\bthe internal\b + - (?i)agones|game ?server|\bfleets?\b + - (?i)secrets-api|pods-api|nodes-api|fleets-api|infra-api|identity-api|billing-api + - (?i)cluster\.local|\.svc\b|vault|kubernetes|\bk8s\b|helm + - (?i)sidecar|webhook|bazel|gcp|gke + +suspect_fields: + - (?i)cluster|node|fleet|spiffe|svid|vault|internal|host|ip|billing|stripe|lease|tenant + +services: + deploys: + path: cmd/apps/deploys-api/api/openapi/api.yaml + operations: + # builds list | describe | create + - GetV1ProjectsProjectIdBuilds + - GetV1BuildsBuildId + - PostV1ProjectsProjectIdBuilds + # deploys list | describe | create | update | delete, deploy + - GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys + - GetV1DeploysDeployId + - PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys + - PutV1DeploysDeployId + - DeleteV1DeploysDeployId + # routes list | create | delete + - GetV1DeploysDeployIdRoutes + - PostV1DeploysDeployIdRoutes + - DeleteV1DeploysDeployIdRoutesRouteId + # domains list | create | verify | delete + - GetV1OrganizationsOrganizationIdDomains + - PostV1OrganizationsOrganizationIdDomains + - PostV1OrganizationsOrganizationIdDomainsDomainVerify + - DeleteV1OrganizationsOrganizationIdDomainsDomain + drop_fields: + # Game-server ports: not part of the CLI and their descriptions are + # about the platform's internals. + - schemas/Build.ports + - requestBodies/CreateBuildRequest.ports + reviewed: + # Returned to customers today; useful for support and debugging. + - schemas/DeployPlacement.cluster_name + # Public custom-domain routing. + - schemas/Route.hostname + - requestBodies/CreateRouteRequest.hostname + descriptions: + "#/components/schemas/DeployEnvVar/description": Environment variable with a plain value. + "#/components/schemas/DeploySecretRef/description": Reference to an environment secret by key; the variable takes the secret's value. + "#/components/schemas/Deploy/properties/env/description": Env entries (plain vars or secret refs). + + org: + path: cmd/apps/org-api/api/openapi/api.yaml + operations: + # orgs list | describe + - GetV1Organizations + - GetV1OrganizationsOrganizationId + # projects list | describe + - GetV1OrganizationsOrganizationIdProjects + - GetV1ProjectsProjectId + # envs list | describe + - GetV1ProjectsProjectIdEnvironments + - GetV1EnvironmentsEnvironmentId + # secrets list | set | delete + - ListEnvironmentSecretsV1 + - CreateEnvironmentSecretV1 + - DeleteEnvironmentSecretV1 + # pods list | describe + - GetV1ProjectsProjectIdPods + - GetV1EnvironmentsEnvironmentIdPods + - GetV1DeploysDeployIdPods + - GetV1PodsPodId + # --location name resolution + - GetV1Locations + drop_fields: + # Game-server host ports: not part of the CLI. + - schemas/Pod.host_ports + reviewed: + # Returned to customers today; useful for support and debugging. + - schemas/Pod.cluster_name + descriptions: + "#/paths/~1v1~1projects~1{project_id}~1environments~1{environment_id}~1secrets/get/description": Lists the secret keys in the environment. Secret values are never returned. + "#/paths/~1v1~1projects~1{project_id}~1environments~1{environment_id}~1secrets~1{secret_name}/post/description": Create or update a secret in the environment. + "#/paths/~1v1~1projects~1{project_id}~1pods/get/description": Lists the pods belonging to the project. + "#/paths/~1v1~1environments~1{environment_id}~1pods/get/description": Lists the pods belonging to the environment. + "#/paths/~1v1~1deploys~1{deploy_id}~1pods/get/description": Lists the pods belonging to the deploy. + "#/paths/~1v1~1pods~1{pod_id}/get/description": Returns a single pod. Returns 404 if the caller is not a member of the pod's organization. + + logs: + path: cmd/apps/logs-api/api/openapi/api.yaml + operations: + # logs (history; --follow uses the WebSocket, hand-written in #254) + - GetV1DeploysDeployIdLogs + + metrics: + path: cmd/apps/metrics-api/api/openapi/api.yaml + operations: + # metrics [--type] + - GetV1MetricTypes + - GetV1DeploysDeployIdMetrics + - GetV1PodsPodIdMetrics diff --git a/internal/api/specs/deploys.yaml b/internal/api/specs/deploys.yaml new file mode 100644 index 0000000..fcc2d4f --- /dev/null +++ b/internal/api/specs/deploys.yaml @@ -0,0 +1,947 @@ +# Code generated by hack/specprune from devlyft/devlyft@bef84e3b430ac77c34dab97cd1c94d976cbdb49d:cmd/apps/deploys-api/api/openapi/api.yaml. DO NOT EDIT. +# Pruned to the operations in allowlist.yaml; regenerate with `make vendor-specs`. +openapi: "3.0.0" +info: + title: Deploys API + version: 1.0.0 +paths: + /v1/projects/{project_id}/builds: + post: + operationId: PostV1ProjectsProjectIdBuilds + summary: Create a new build + description: Create a new build within the specified project. + tags: + - Builds + parameters: + - name: project_id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + $ref: '#/components/requestBodies/CreateBuildRequest' + responses: + '201': + description: Build created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Build' + '400': + description: Invalid input + '404': + description: Project not found + '409': + description: Build with the same name already exists in this project + '500': + description: Internal server error + get: + operationId: GetV1ProjectsProjectIdBuilds + summary: List builds for a project + description: Returns a list of all builds for the specified project. + tags: + - Builds + parameters: + - name: project_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: List of builds + content: + application/json: + schema: + $ref: '#/components/schemas/ListBuildsResponse' + '404': + description: Project not found + '500': + description: Internal server error + /v1/builds/{build_id}: + get: + operationId: GetV1BuildsBuildId + summary: Get a build by ID + description: Returns the build with the given ID. + tags: + - Builds + parameters: + - name: build_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Build found + content: + application/json: + schema: + $ref: '#/components/schemas/Build' + '404': + description: Build not found + '500': + description: Internal server error + /v1/projects/{project_id}/environments/{environment_id}/deploys: + post: + operationId: PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys + summary: Create a new deploy + description: Create a new deploy in the specified environment. + tags: + - Deploys + parameters: + - name: project_id + in: path + required: true + schema: + type: string + format: uuid + - name: environment_id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + $ref: '#/components/requestBodies/CreateDeployRequest' + responses: + '201': + description: Deploy created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Deploy' + '400': + description: Invalid input + '402': + description: Organization has not completed billing setup + '404': + description: Project, environment, or build not found + '409': + description: Deploy with the same name already exists in this environment + '500': + description: Internal server error + get: + operationId: GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys + summary: List deploys in an environment + description: Returns all deploys in the specified environment. + tags: + - Deploys + parameters: + - name: project_id + in: path + required: true + schema: + type: string + format: uuid + - name: environment_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: List of deploys + content: + application/json: + schema: + $ref: '#/components/schemas/ListDeploysResponse' + '404': + description: Project or environment not found + '500': + description: Internal server error + /v1/deploys/{deploy_id}: + get: + operationId: GetV1DeploysDeployId + summary: Get a deploy by ID + description: Returns the deploy with the given ID. + tags: + - Deploys + parameters: + - name: deploy_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Deploy found + content: + application/json: + schema: + $ref: '#/components/schemas/Deploy' + '404': + description: Deploy not found + '500': + description: Internal server error + put: + operationId: PutV1DeploysDeployId + summary: Update a deploy + description: Update a deploy with the given ID. + tags: + - Deploys + parameters: + - name: deploy_id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + $ref: '#/components/requestBodies/UpdateDeployRequest' + responses: + '200': + description: Deploy updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Deploy' + '400': + description: Invalid input + '402': + description: Organization has not completed billing setup + '404': + description: Deploy or Build not found + '409': + description: Deploy with the same name already exists in this environment + '500': + description: Internal server error + delete: + operationId: DeleteV1DeploysDeployId + summary: Delete a deploy + description: Delete a deploy with the given ID. + tags: + - Deploys + parameters: + - name: deploy_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '204': + description: Deploy deleted successfully + '404': + description: Deploy not found + '500': + description: Internal server error + /v1/organizations/{organization_id}/domains: + post: + operationId: PostV1OrganizationsOrganizationIdDomains + summary: Claim a custom domain + description: Claim a custom domain for the organization; returns the DNS TXT challenge to publish. + tags: + - CustomDomains + parameters: + - name: organization_id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + $ref: '#/components/requestBodies/CreateCustomDomainRequest' + responses: + '201': + description: Custom domain claimed + content: + application/json: + schema: + $ref: '#/components/schemas/CustomDomain' + '400': + description: Invalid input + '404': + description: Organization not found + '409': + description: Domain is already claimed + '500': + description: Internal server error + get: + operationId: GetV1OrganizationsOrganizationIdDomains + summary: List custom domains for an organization + description: Returns all custom domains owned by the organization. + tags: + - CustomDomains + parameters: + - name: organization_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: List of custom domains + content: + application/json: + schema: + $ref: '#/components/schemas/ListCustomDomainsResponse' + '404': + description: Organization not found + '500': + description: Internal server error + /v1/organizations/{organization_id}/domains/{domain}/verify: + post: + operationId: PostV1OrganizationsOrganizationIdDomainsDomainVerify + summary: Verify a custom domain now + description: Performs the DNS TXT ownership check immediately and returns the updated domain. + tags: + - CustomDomains + parameters: + - name: organization_id + in: path + required: true + schema: + type: string + format: uuid + - name: domain + in: path + required: true + schema: + type: string + responses: + '200': + description: Verification attempted; current domain state returned + content: + application/json: + schema: + $ref: '#/components/schemas/CustomDomain' + '404': + description: Custom domain not found + '500': + description: Internal server error + /v1/organizations/{organization_id}/domains/{domain}: + delete: + operationId: DeleteV1OrganizationsOrganizationIdDomainsDomain + summary: Delete a custom domain + description: Removes a custom domain from the organization. + tags: + - CustomDomains + parameters: + - name: organization_id + in: path + required: true + schema: + type: string + format: uuid + - name: domain + in: path + required: true + schema: + type: string + responses: + '204': + description: Custom domain deleted successfully + '404': + description: Custom domain not found + '500': + description: Internal server error + /v1/deploys/{deploy_id}/routes: + post: + operationId: PostV1DeploysDeployIdRoutes + summary: Attach a hostname (and optional path) to a deploy + description: Route a hostname covered by an org-verified domain to this deploy, optionally scoped to a path prefix. + tags: + - Routes + parameters: + - name: deploy_id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + $ref: '#/components/requestBodies/CreateRouteRequest' + responses: + '201': + description: Route created + content: + application/json: + schema: + $ref: '#/components/schemas/Route' + '400': + description: Hostname not covered by a verified domain, or cross-environment conflict + '404': + description: Deploy not found + '409': + description: Host and path already routed + '500': + description: Internal server error + get: + operationId: GetV1DeploysDeployIdRoutes + summary: List a deploy's routes + tags: + - Routes + parameters: + - name: deploy_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: List of routes + content: + application/json: + schema: + $ref: '#/components/schemas/ListRoutesResponse' + '404': + description: Deploy not found + '500': + description: Internal server error + /v1/deploys/{deploy_id}/routes/{route_id}: + delete: + operationId: DeleteV1DeploysDeployIdRoutesRouteId + summary: Detach a route from a deploy + tags: + - Routes + parameters: + - name: deploy_id + in: path + required: true + schema: + type: string + format: uuid + - name: route_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '204': + description: Route deleted successfully + '404': + description: Route not found + '500': + description: Internal server error +components: + schemas: + DeployPlacementState: + type: string + enum: + - PLACED + - PENDING + description: >- + Whether the deploy has been assigned to an edge cluster. PENDING means no cluster in the deploy's location can currently take it, most often because the cluster its environment occupies is full or is being drained. A deploy stays PENDING until capacity frees up; it is retried automatically and needs no action to resume. + DeployPlacement: + type: object + description: >- + Where the deploy is running. All deploys of one environment in one location share a cluster, so this is a property of the environment's placement as much as the deploy's. + properties: + state: + $ref: '#/components/schemas/DeployPlacementState' + cluster_name: + type: string + description: The name of the edge cluster serving this deploy. Absent while state is PENDING. + required: + - state + TenancyType: + type: string + enum: + - INHERIT + - SHARED + - DEDICATED + description: >- + Type of tenancy for the deploy. INHERIT follows the environment's tenancy policy; SHARED forces the global shared pool; DEDICATED leases nodes to this deploy exclusively. + DeployEnvVar: + type: object + description: Environment variable with a plain value. + required: + - kind + - key + - value + properties: + kind: + type: string + enum: + - env + description: Identifies this entry as an env var + key: + type: string + description: Variable name + value: + type: string + description: Plain value + DeploySecretRef: + type: object + description: Reference to an environment secret by key; the variable takes the secret's value. + required: + - kind + - key + properties: + kind: + type: string + enum: + - secret + description: Identifies this entry as a secret ref + key: + type: string + description: Secret key name + DeployEnvItem: + description: One env entry — user specifies kind (env or secret) to indicate plain value vs secret ref. + oneOf: + - $ref: '#/components/schemas/DeployEnvVar' + - $ref: '#/components/schemas/DeploySecretRef' + discriminator: + propertyName: kind + Build: + type: object + properties: + id: + type: string + format: uuid + description: UUID of the build + project_id: + type: string + format: uuid + description: UUID of the parent project + name: + type: string + description: Name of the build + image: + type: string + description: Container image for the build + tag: + type: string + description: Tag for the build + cpu_request: + type: string + description: CPU request (e.g., "100m", "1") + memory_request: + type: string + description: Memory request (e.g., "128Mi", "1Gi") + container_port: + type: integer + nullable: true + minimum: 1 + maximum: 65535 + description: Port the container listens on; required to expose a deploy built from this build + created_at: + type: string + format: date-time + description: Timestamp when the build was created + updated_at: + type: string + format: date-time + description: Timestamp when the build was last updated + required: + - id + - project_id + - name + - image + - tag + - created_at + - updated_at + - cpu_request + - memory_request + Deploy: + type: object + properties: + id: + type: string + format: uuid + description: UUID of the deploy + environment_id: + type: string + format: uuid + description: UUID of the parent environment + build_id: + type: string + format: uuid + description: UUID of the build used by this deploy + name: + type: string + description: Name of the deploy + location_id: + type: string + description: Location ID where the deploy is deployed + env: + type: array + items: + $ref: '#/components/schemas/DeployEnvItem' + nullable: true + description: Env entries (plain vars or secret refs). + tenancy_type: + $ref: '#/components/schemas/TenancyType' + instance_type: + type: string + description: Dedicated node instance type (present when the deploy pins one). + replicas: + type: integer + format: uint32 + description: Number of replicas + expose: + type: boolean + description: Whether the deploy is publicly routed via edge ingress + enable_hpa: + type: boolean + description: Whether HPA is enabled + hpa_min_replicas: + type: integer + format: uint32 + nullable: true + description: Minimum replicas for HPA + hpa_max_replicas: + type: integer + format: uint32 + nullable: true + description: Maximum replicas for HPA + hpa_target_cpu: + type: integer + format: uint32 + nullable: true + description: Target CPU percentage for HPA + placement: + $ref: '#/components/schemas/DeployPlacement' + has_registry_credential: + type: boolean + description: Whether a private container registry pull credential is set for this deploy's image. The credential value itself is never returned. + registry_credential_name: + type: string + nullable: true + description: Name of the saved registry credential this deploy uses. Null when it has none. + created_at: + type: string + format: date-time + description: Timestamp when the deploy was created + updated_at: + type: string + format: date-time + description: Timestamp when the deploy was last updated + required: + - id + - environment_id + - build_id + - name + - location_id + - tenancy_type + - replicas + - expose + - enable_hpa + - placement + - has_registry_credential + - created_at + - updated_at + ListBuildsResponse: + type: object + properties: + builds: + type: array + items: + $ref: '#/components/schemas/Build' + required: + - builds + ListDeploysResponse: + type: object + properties: + deploys: + type: array + items: + $ref: '#/components/schemas/Deploy' + required: + - deploys + CustomDomain: + type: object + description: A customer-owned domain and its ownership-verification state. + properties: + id: + type: string + format: uuid + description: UUID of the custom domain + organization_id: + type: string + format: uuid + description: UUID of the owning organization + domain: + type: string + description: The owned domain; verification covers it and all of its subdomains + status: + type: string + enum: + - pending + - verified + - active + - failed + - revoked + description: Ownership-verification state + verification_token: + type: string + description: Value to publish in the TXT challenge record + challenge_record_name: + type: string + description: DNS name to create a TXT record at (its value must equal verification_token) + verified_at: + type: string + format: date-time + nullable: true + description: When ownership was first verified + last_checked_at: + type: string + format: date-time + nullable: true + description: When verification was last attempted + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + required: + - id + - organization_id + - domain + - status + - verification_token + - challenge_record_name + - created_at + - updated_at + ListCustomDomainsResponse: + type: object + properties: + domains: + type: array + items: + $ref: '#/components/schemas/CustomDomain' + required: + - domains + Route: + type: object + description: A hostname (optionally scoped to a path prefix) routed to a deploy. + properties: + id: + type: string + format: uuid + deploy_id: + type: string + format: uuid + hostname: + type: string + path_prefix: + type: string + cname_target: + type: string + description: >- + The name to point this hostname's CNAME at. It is derived from the hostname and never changes for the life of the route, including when the deploy moves between edge clusters — which is the point: the customer publishes this once and never edits their DNS again. Absent when the deployment publishes no edge DNS. + example: d0c43d3885064d9a.edge.devlyft.io + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + required: + - id + - deploy_id + - hostname + - path_prefix + - created_at + - updated_at + ListRoutesResponse: + type: object + properties: + routes: + type: array + items: + $ref: '#/components/schemas/Route' + required: + - routes + requestBodies: + CreateRouteRequest: + description: Request body for attaching a route to a deploy + required: true + content: + application/json: + schema: + type: object + properties: + hostname: + type: string + description: Hostname to route; must be covered by an org-verified domain + path_prefix: + type: string + description: Optional path prefix (e.g. /v1/one); omit or "/" for the whole host + required: + - hostname + CreateCustomDomainRequest: + description: Request body for claiming a custom domain + required: true + content: + application/json: + schema: + type: object + properties: + domain: + type: string + description: >- + The domain the organization owns and can prove control of, e.g. "devlyft.io". Ownership is verified once (via a TXT record at _devlyft-challenge.) and covers this domain and all of its subdomains, which may then be routed to deploys. + required: + - domain + CreateBuildRequest: + description: Request body for creating a new build + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: Name of the build + image: + type: string + description: Container image for the build + tag: + type: string + description: Tag for the build + cpu_request: + type: string + description: CPU request (e.g., "100m", "1") + memory_request: + type: string + description: Memory request (e.g., "128Mi", "1Gi") + container_port: + type: integer + nullable: true + minimum: 1 + maximum: 65535 + description: Port the container listens on; required to expose a deploy built from this build + required: + - name + - image + - tag + - cpu_request + - memory_request + CreateDeployRequest: + description: Request body for creating a new deploy (environment is in the path). + required: true + content: + application/json: + schema: + type: object + properties: + build_id: + type: string + format: uuid + description: UUID of the build to use + name: + type: string + description: Name of the deploy + location_id: + type: string + description: Location ID where the deploy is deployed + env: + type: array + items: + $ref: '#/components/schemas/DeployEnvItem' + nullable: true + description: Env entries (plain vars or secret refs) + tenancy_type: + $ref: '#/components/schemas/TenancyType' + instance_type: + type: string + description: Dedicated node instance type (e.g. devlyft-amd-balanced-2). Required when the deploy is DEDICATED; INHERIT deploys inherit the environment's. + replicas: + type: integer + format: uint32 + description: Number of replicas + expose: + type: boolean + description: Publicly route this deploy via edge ingress (requires the build to declare a container_port) + enable_hpa: + type: boolean + description: Whether HPA is enabled + hpa_min_replicas: + type: integer + format: uint32 + nullable: true + description: Minimum replicas for HPA + hpa_max_replicas: + type: integer + format: uint32 + nullable: true + description: Maximum replicas for HPA + hpa_target_cpu: + type: integer + format: uint32 + nullable: true + description: Target CPU percentage for HPA + registry_credential_name: + type: string + description: Name of a registry credential saved in the deploy's environment, used to pull a private image. Omit for a public image. + required: + - build_id + - name + - location_id + - replicas + - enable_hpa + UpdateDeployRequest: + description: Request body for updating a deploy + required: true + content: + application/json: + schema: + type: object + properties: + build_id: + type: string + format: uuid + description: UUID of the build to use + name: + type: string + description: Name of the deploy + location_id: + type: string + description: Location ID where the deploy is deployed + env: + type: array + items: + $ref: '#/components/schemas/DeployEnvItem' + nullable: true + description: Env entries (plain vars or secret refs) + tenancy_type: + $ref: '#/components/schemas/TenancyType' + instance_type: + type: string + description: Dedicated node instance type (e.g. devlyft-amd-balanced-2). Required when the deploy is DEDICATED; INHERIT deploys inherit the environment's. + replicas: + type: integer + format: uint32 + description: Number of replicas + expose: + type: boolean + description: Publicly route this deploy via edge ingress (requires the build to declare a container_port) + enable_hpa: + type: boolean + description: Whether HPA is enabled + hpa_min_replicas: + type: integer + format: uint32 + nullable: true + description: Minimum replicas for HPA + hpa_max_replicas: + type: integer + format: uint32 + nullable: true + description: Maximum replicas for HPA + hpa_target_cpu: + type: integer + format: uint32 + nullable: true + description: Target CPU percentage for HPA + registry_credential_name: + type: string + description: Name of a registry credential saved in the deploy's environment, used to pull a private image. Omit to keep the deploy's current credential; detach one with DELETE /v1/deploys/{deploy_id}/registry-credential. + required: + - build_id + - name + - location_id + - replicas + - enable_hpa diff --git a/internal/api/specs/logs.yaml b/internal/api/specs/logs.yaml new file mode 100644 index 0000000..dc9301c --- /dev/null +++ b/internal/api/specs/logs.yaml @@ -0,0 +1,101 @@ +# Code generated by hack/specprune from devlyft/devlyft@bef84e3b430ac77c34dab97cd1c94d976cbdb49d:cmd/apps/logs-api/api/openapi/api.yaml. DO NOT EDIT. +# Pruned to the operations in allowlist.yaml; regenerate with `make vendor-specs`. +openapi: "3.0.0" +info: + title: Logs API + version: 1.0.0 +paths: + /v1/deploys/{deployId}/logs: + get: + operationId: GetV1DeploysDeployIdLogs + summary: Read recent logs for a deploy + description: | + Returns a page of recent log lines for the deploy (optionally narrowed to a single pod), most + recent last. The caller must be a member of the organization that owns the deploy. `cursor` is + the timestamp (RFC3339Nano) after which a live tail should continue. + parameters: + - name: deployId + in: path + required: true + schema: + type: string + format: uuid + - name: pod + in: query + description: Narrow to a single pod name. + required: false + schema: + type: string + - name: since + in: query + description: How far back to read (Go duration, e.g. "1h"). Defaults to 1h. + required: false + schema: + type: string + - name: filter + in: query + description: Case-insensitive substring the log line must contain. + required: false + schema: + type: string + - name: match + in: query + description: | + Structured field filters applied to the log's JSON fields, each `` where + op is one of `=`, `!=`, `=~`, `!~` (e.g. `status=running`, `service=~api-.*`). Repeatable; + all must match. Only JSON log lines can match a field filter. + required: false + explode: true + style: form + schema: + type: array + items: + type: string + - name: limit + in: query + description: Max number of lines (default 500, max 5000). + required: false + schema: + type: integer + format: int32 + responses: + "200": + description: A page of log entries. + content: + application/json: + schema: + $ref: "#/components/schemas/LogsResponse" + "403": + description: The caller is not a member of the owning organization. + "404": + description: The deploy does not exist (or is not visible to the caller). +components: + schemas: + LogEntry: + type: object + properties: + timestamp: + type: string + description: RFC3339Nano timestamp of the log line. + line: + type: string + description: The raw log line. + pod: + type: string + description: The pod the line came from. + required: + - timestamp + - line + LogsResponse: + type: object + properties: + entries: + type: array + items: + $ref: "#/components/schemas/LogEntry" + cursor: + type: string + description: RFC3339Nano timestamp to continue a live tail from (latest entry, or the query start when empty). + required: + - entries + - cursor diff --git a/internal/api/specs/metrics.yaml b/internal/api/specs/metrics.yaml new file mode 100644 index 0000000..2042104 --- /dev/null +++ b/internal/api/specs/metrics.yaml @@ -0,0 +1,169 @@ +# Code generated by hack/specprune from devlyft/devlyft@bef84e3b430ac77c34dab97cd1c94d976cbdb49d:cmd/apps/metrics-api/api/openapi/api.yaml. DO NOT EDIT. +# Pruned to the operations in allowlist.yaml; regenerate with `make vendor-specs`. +openapi: "3.0.0" +info: + title: Metrics API + version: 1.0.0 +paths: + /v1/metric-types: + get: + operationId: GetV1MetricTypes + summary: List supported curated metric types + description: The curated metric keys (e.g. cpu, memory) accepted by the deploy and pod endpoints. + responses: + "200": + description: The curated metric catalog. + content: + application/json: + schema: + $ref: "#/components/schemas/MetricTypesResponse" + /v1/deploys/{deployId}/metrics: + get: + operationId: GetV1DeploysDeployIdMetrics + summary: Read a curated metric time series for a deploy + description: | + Returns a curated metric as a time series for the deploy (optionally narrowed to a single pod) + over [now-range, now] at the given step. The caller must be a member of the organization that + owns the deploy. + parameters: + - name: deployId + in: path + required: true + schema: + type: string + format: uuid + - name: metric + in: query + required: true + description: Curated metric key (see GET /v1/metric-types). + schema: + type: string + - name: pod + in: query + required: false + description: Narrow to a single pod name. + schema: + type: string + - name: range + in: query + required: false + description: How far back to read (Go duration, e.g. "1h"). Defaults to 1h. + schema: + type: string + - name: step + in: query + required: false + description: Resolution between points (Go duration, e.g. "30s"). Defaults to ~120 points over the range. + schema: + type: string + responses: + "200": + description: The metric time series. + content: + application/json: + schema: + $ref: "#/components/schemas/MetricsResponse" + "400": + description: Unknown metric or invalid duration. + "403": + description: The caller is not a member of the owning organization. + "404": + description: The deploy does not exist (or is not visible to the caller). + /v1/pods/{podId}/metrics: + get: + operationId: GetV1PodsPodIdMetrics + summary: Read a curated metric time series for a single pod + description: | + Returns a curated metric as a time series for one pod over [now-range, now] at the given step. + The owning deploy is derived from the pod name; the caller must be a member of that deploy's + organization. + parameters: + - name: podId + in: path + required: true + description: The pod name. + schema: + type: string + - name: metric + in: query + required: true + description: Curated metric key (see GET /v1/metric-types). + schema: + type: string + - name: range + in: query + required: false + description: How far back to read (Go duration, e.g. "1h"). Defaults to 1h. + schema: + type: string + - name: step + in: query + required: false + description: Resolution between points (Go duration, e.g. "30s"). Defaults to ~120 points over the range. + schema: + type: string + responses: + "200": + description: The metric time series. + content: + application/json: + schema: + $ref: "#/components/schemas/MetricsResponse" + "400": + description: Unknown metric, invalid duration, or unrecognizable pod name. + "403": + description: The caller is not a member of the owning organization. + "404": + description: The pod's deploy does not exist (or is not visible to the caller). +components: + schemas: + MetricTypesResponse: + type: object + properties: + metrics: + type: array + items: + type: string + description: Supported curated metric keys. + required: + - metrics + MetricPoint: + type: object + properties: + timestamp: + type: string + description: RFC3339 timestamp of the sample. + value: + type: number + format: double + required: + - timestamp + - value + MetricSeries: + type: object + properties: + labels: + type: object + additionalProperties: + type: string + description: The series' label set (e.g. pod, deploy_id). + points: + type: array + items: + $ref: "#/components/schemas/MetricPoint" + required: + - labels + - points + MetricsResponse: + type: object + properties: + metric: + type: string + description: The curated metric key that was queried. + series: + type: array + items: + $ref: "#/components/schemas/MetricSeries" + required: + - metric + - series diff --git a/internal/api/specs/org.yaml b/internal/api/specs/org.yaml new file mode 100644 index 0000000..dfc7210 --- /dev/null +++ b/internal/api/specs/org.yaml @@ -0,0 +1,660 @@ +# Code generated by hack/specprune from devlyft/devlyft@bef84e3b430ac77c34dab97cd1c94d976cbdb49d:cmd/apps/org-api/api/openapi/api.yaml. DO NOT EDIT. +# Pruned to the operations in allowlist.yaml; regenerate with `make vendor-specs`. +openapi: "3.0.0" +info: + title: Organization API + version: 1.0.0 +paths: + /v1/organizations: + get: + operationId: GetV1Organizations + summary: List all organizations + description: Returns a list of all organizations. + tags: + - Organizations + responses: + '200': + description: List of organizations + content: + application/json: + schema: + $ref: '#/components/schemas/ListOrganizationsResponse' + '500': + description: Internal server error + /v1/organizations/{organization_id}: + get: + operationId: GetV1OrganizationsOrganizationId + summary: Get an organization by ID + description: Returns the organization with the given ID. + tags: + - Organizations + parameters: + - name: organization_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Organization found + content: + application/json: + schema: + $ref: '#/components/schemas/Organization' + '404': + description: Organization not found + '500': + description: Internal server error + /v1/organizations/{organization_id}/projects: + get: + operationId: GetV1OrganizationsOrganizationIdProjects + summary: List projects for an organization + description: Returns a list of all projects for the specified organization. + tags: + - Projects + parameters: + - name: organization_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: List of projects + content: + application/json: + schema: + $ref: '#/components/schemas/ListProjectsResponse' + '404': + description: Organization not found + '500': + description: Internal server error + /v1/environments/{environment_id}: + get: + operationId: GetV1EnvironmentsEnvironmentId + summary: Get an environment by ID + description: Returns the environment with the given ID. + tags: + - Environments + parameters: + - name: environment_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Environment found + content: + application/json: + schema: + $ref: '#/components/schemas/Environment' + '404': + description: Environment not found + '500': + description: Internal server error + /v1/projects/{project_id}/environments: + get: + operationId: GetV1ProjectsProjectIdEnvironments + summary: List environments for a project + description: Returns a list of all environments for the specified project. + tags: + - Environments + parameters: + - name: project_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: List of environments + content: + application/json: + schema: + $ref: '#/components/schemas/ListEnvironmentsResponse' + '404': + description: Project not found + '500': + description: Internal server error + /v1/projects/{project_id}: + get: + operationId: GetV1ProjectsProjectId + summary: Get a project by ID + description: Returns the project with the given ID. + tags: + - Projects + parameters: + - name: project_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Project found + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + '404': + description: Project not found + '500': + description: Internal server error + /v1/projects/{project_id}/environments/{environment_id}/secrets: + parameters: + - name: project_id + in: path + required: true + schema: + type: string + format: uuid + - name: environment_id + in: path + required: true + schema: + type: string + format: uuid + get: + operationId: ListEnvironmentSecretsV1 + summary: List environment secrets + description: Lists the secret keys in the environment. Secret values are never returned. + tags: + - Secrets + parameters: + - name: limit + in: query + required: false + schema: + type: integer + format: int32 + minimum: 1 + maximum: 100 + default: 25 + - name: offset + in: query + required: false + schema: + type: integer + format: int32 + minimum: 0 + default: 0 + responses: + '200': + description: List of environment secrets + content: + application/json: + schema: + $ref: '#/components/schemas/ListEnvironmentSecretsResponse' + '403': + description: Caller is not permitted to view this environment's secrets + '404': + description: Environment not found + '500': + description: Internal server error + /v1/projects/{project_id}/environments/{environment_id}/secrets/{secret_name}: + parameters: + - name: project_id + in: path + required: true + schema: + type: string + format: uuid + - name: environment_id + in: path + required: true + schema: + type: string + format: uuid + - name: secret_name + in: path + required: true + schema: + type: string + post: + operationId: CreateEnvironmentSecretV1 + summary: Create or update an environment secret + description: Create or update a secret in the environment. + tags: + - Secrets + requestBody: + $ref: '#/components/requestBodies/CreateEnvironmentSecretRequest' + responses: + '201': + description: Secret persisted + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentSecret' + '400': + description: Invalid input + '403': + description: Caller is not permitted to manage this environment's secrets + '404': + description: Environment not found + '500': + description: Internal server error + delete: + operationId: DeleteEnvironmentSecretV1 + summary: Delete an environment secret + tags: + - Secrets + responses: + '204': + description: Secret deleted + '403': + description: Caller is not permitted to manage this environment's secrets + '404': + description: Secret not found + '500': + description: Internal server error + /v1/projects/{project_id}/pods: + parameters: + - name: project_id + in: path + required: true + schema: + type: string + format: uuid + get: + operationId: GetV1ProjectsProjectIdPods + summary: List pods in a project + description: Lists the pods belonging to the project. + tags: + - Pods + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: List of pods + content: + application/json: + schema: + $ref: '#/components/schemas/ListPodsResponse' + '403': + description: Caller is not permitted to view this project's pods + '404': + description: Project not found + '500': + description: Internal server error + /v1/environments/{environment_id}/pods: + parameters: + - name: environment_id + in: path + required: true + schema: + type: string + format: uuid + get: + operationId: GetV1EnvironmentsEnvironmentIdPods + summary: List pods in an environment + description: Lists the pods belonging to the environment. + tags: + - Pods + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: List of pods + content: + application/json: + schema: + $ref: '#/components/schemas/ListPodsResponse' + '403': + description: Caller is not permitted to view this environment's pods + '404': + description: Environment not found + '500': + description: Internal server error + /v1/deploys/{deploy_id}/pods: + parameters: + - name: deploy_id + in: path + required: true + schema: + type: string + format: uuid + get: + operationId: GetV1DeploysDeployIdPods + summary: List pods in a deploy + description: Lists the pods belonging to the deploy. + tags: + - Pods + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: List of pods + content: + application/json: + schema: + $ref: '#/components/schemas/ListPodsResponse' + '403': + description: Caller is not permitted to view this deploy's pods + '404': + description: Deploy not found + '500': + description: Internal server error + /v1/pods/{pod_id}: + parameters: + - name: pod_id + in: path + required: true + schema: + type: string + get: + operationId: GetV1PodsPodId + summary: Get a pod + description: Returns a single pod. Returns 404 if the caller is not a member of the pod's organization. + tags: + - Pods + responses: + '200': + description: The pod + content: + application/json: + schema: + $ref: '#/components/schemas/Pod' + '404': + description: Pod not found + '500': + description: Internal server error + /v1/locations: + get: + operationId: GetV1Locations + summary: List available locations + description: | + Returns the locations (regions) where workloads can be deployed. This endpoint is public and + unauthenticated so it can back a signup/region picker before a user has any credential. + tags: + - Locations + responses: + '200': + description: List of locations + content: + application/json: + schema: + $ref: '#/components/schemas/ListLocationsResponse' + '500': + description: Internal server error +components: + schemas: + Location: + type: object + properties: + id: + type: string + description: The location identifier (region name). + required: + - id + ListLocationsResponse: + type: object + properties: + locations: + type: array + items: + $ref: '#/components/schemas/Location' + required: + - locations + EnvironmentSecret: + type: object + properties: + key: + type: string + description: The name/key of the secret. The value is never returned. + required: + - key + ListEnvironmentSecretsResponse: + type: object + properties: + secrets: + type: array + items: + $ref: '#/components/schemas/EnvironmentSecret' + limit: + type: integer + format: int32 + description: Maximum number of secrets returned. + offset: + type: integer + format: int32 + description: Offset of the first secret returned. + total: + type: integer + format: int64 + description: Total number of secrets in the environment. + required: + - secrets + - limit + - offset + - total + Organization: + type: object + properties: + id: + type: string + format: uuid + description: UUID of the organization + name: + type: string + description: Display name of the organization. Free-form text; not unique. + slug: + type: string + description: >- + Unique, immutable, URL-safe identifier of the organization (3-39 characters, lowercase a-z, digits and single hyphens). Usable wherever an organization id is accepted by the CLI. + created_at: + type: string + format: date-time + description: Timestamp when the organization was created + updated_at: + type: string + format: date-time + description: Timestamp when the organization was last updated + required: + - id + - name + - slug + - created_at + - updated_at + Project: + type: object + properties: + id: + type: string + format: uuid + description: UUID of the project + organization_id: + type: string + format: uuid + description: UUID of the parent organization + name: + type: string + description: Name of the project + created_at: + type: string + format: date-time + description: Timestamp when the project was created + updated_at: + type: string + format: date-time + description: Timestamp when the project was last updated + required: + - id + - organization_id + - name + - created_at + - updated_at + EnvironmentTenancyType: + type: string + enum: + - SHARED + - DEDICATED + description: >- + Tenancy policy for the environment. SHARED places inheriting deploys on the global shared pool; DEDICATED leases nodes to the environment so its deploys co-tenant on environment-leased nodes (a deploy can still override to its own dedicated nodes). + Environment: + type: object + properties: + id: + type: string + format: uuid + description: UUID of the environment + project_id: + type: string + format: uuid + description: UUID of the parent project + name: + type: string + description: Name of the environment + tenancy_type: + $ref: '#/components/schemas/EnvironmentTenancyType' + instance_type: + type: string + description: Dedicated node instance type (present when set); required for DEDICATED environments. + created_at: + type: string + format: date-time + description: Timestamp when the environment was created + updated_at: + type: string + format: date-time + description: Timestamp when the environment was last updated + required: + - id + - project_id + - name + - tenancy_type + - created_at + - updated_at + ListOrganizationsResponse: + type: object + properties: + organizations: + type: array + items: + $ref: '#/components/schemas/Organization' + required: + - organizations + ListEnvironmentsResponse: + type: object + properties: + environments: + type: array + items: + $ref: '#/components/schemas/Environment' + required: + - environments + ListProjectsResponse: + type: object + properties: + projects: + type: array + items: + $ref: '#/components/schemas/Project' + required: + - projects + Pod: + type: object + description: A pod belonging to one of the caller's deploys. + properties: + id: + type: string + description: The ID of the pod. + name: + type: string + description: The name of the pod. + phase: + type: string + description: The phase the pod is in. + phase_reason: + type: string + description: The reason for the pod being in its current phase. + location_id: + type: string + description: The location ID of the pod. + cluster_name: + type: string + description: The name of the cluster the pod is in. + project_id: + type: string + description: The project the pod belongs to. + environment_id: + type: string + description: The environment the pod belongs to. + deploy_id: + type: string + description: The deploy the pod belongs to. + required: + - id + - name + - phase + - location_id + - cluster_name + - project_id + - environment_id + - deploy_id + ListPodsResponse: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/Pod' + limit: + type: integer + format: int32 + description: Maximum number of pods returned. + offset: + type: integer + format: int32 + description: Offset of the first pod returned. + total: + type: integer + format: int64 + description: Total number of pods matching the query. + required: + - results + - limit + - offset + - total + parameters: + Limit: + name: limit + in: query + required: false + schema: + type: integer + format: int32 + minimum: 1 + maximum: 100 + default: 25 + Offset: + name: offset + in: query + required: false + schema: + type: integer + format: int32 + minimum: 0 + default: 0 + requestBodies: + CreateEnvironmentSecretRequest: + description: Request body for creating or updating an environment secret + required: true + content: + application/json: + schema: + type: object + properties: + value: + type: string + description: The secret value. + required: + - value From 434ec44c0d08a8ff4dfdb5098bb75e7fd228ebe9 Mon Sep 17 00:00:00 2001 From: RogerCecilpot Date: Sun, 25 Oct 2026 12:44:13 +0000 Subject: [PATCH 2/3] feat: API facade with retries, read-modify-write PUT and pagination internal/api.Client is every call the tier 1 and 2 commands make, over the generated clients, with a gomock mock in internal/api/mock. - GET, PUT and DELETE retry on 429, 5xx (not 501) and connection errors with exponential backoff and full jitter, honouring Retry-After in seconds or as a date; POST is never retried. Attempts and total wait are capped, cancellation stops retries, and the final error states the attempt count. - The bearer token is asked for on every attempt, so refreshing token sources plug in; tokens are only sent over HTTPS or to loopback. - Non-2xx responses map to clierr errors with the server's message and request ID (X-Request-Id header or body field). - UpdateDeploy reads the deploy, applies a mutation to a full update request and writes it back, sending If-Match when the GET returns an ETag. - Paged collects from limit/offset endpoints; Trim limits the rest. - Each exchange is debug-logged through internal/redact. - EnvVarItem and SecretRefItem build env entries with the right kind: the spec's discriminator has no mapping, so the generated helpers use schema names as the kind, which the API rejects. Contract tests validate every request and response against the vendored specs with kin-openapi and check every vendored operation is used. Refs devlyft/devlyft#236 Co-Authored-By: Claude Opus 5.5 --- internal/api/client.go | 205 +++++++++++++ internal/api/client_test.go | 208 +++++++++++++ internal/api/contract_test.go | 259 ++++++++++++++++ internal/api/deploys.go | 290 ++++++++++++++++++ internal/api/envitem.go | 55 ++++ internal/api/envitem_test.go | 70 +++++ internal/api/errors.go | 103 +++++++ internal/api/mock/client.go | 541 +++++++++++++++++++++++++++++++++ internal/api/mock/mock_test.go | 26 ++ internal/api/observe.go | 116 +++++++ internal/api/org.go | 247 +++++++++++++++ internal/api/paging.go | 49 +++ internal/api/paging_test.go | 104 +++++++ internal/api/transport.go | 284 +++++++++++++++++ internal/api/transport_test.go | 446 +++++++++++++++++++++++++++ 15 files changed, 3003 insertions(+) create mode 100644 internal/api/client.go create mode 100644 internal/api/client_test.go create mode 100644 internal/api/contract_test.go create mode 100644 internal/api/deploys.go create mode 100644 internal/api/envitem.go create mode 100644 internal/api/envitem_test.go create mode 100644 internal/api/errors.go create mode 100644 internal/api/mock/client.go create mode 100644 internal/api/mock/mock_test.go create mode 100644 internal/api/observe.go create mode 100644 internal/api/org.go create mode 100644 internal/api/paging.go create mode 100644 internal/api/paging_test.go create mode 100644 internal/api/transport.go create mode 100644 internal/api/transport_test.go diff --git a/internal/api/client.go b/internal/api/client.go new file mode 100644 index 0000000..999cefa --- /dev/null +++ b/internal/api/client.go @@ -0,0 +1,205 @@ +// Package api is the thin facade over the generated DevLyft API clients. It +// is the only package that knows which service owns which endpoint. +// +// Commands depend on the Client interface, never on the generated packages, +// and are tested against the gomock mock in internal/api/mock. +package api + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net" + "net/http" + "net/url" + "runtime" + "strings" + + "github.com/google/uuid" + + "github.com/devlyft/cli/internal/api/gen/deploys" + "github.com/devlyft/cli/internal/api/gen/logs" + "github.com/devlyft/cli/internal/api/gen/metrics" + "github.com/devlyft/cli/internal/api/gen/org" + "github.com/devlyft/cli/internal/version" +) + +//go:generate go tool mockgen -source=client.go -destination=mock/client.go -package=mock + +// Resource types, re-exported so commands need not import the generated +// packages, where they are documented. +// +//nolint:revive // aliases +type ( + Organization = org.Organization + Project = org.Project + Environment = org.Environment + EnvironmentSecret = org.EnvironmentSecret + Pod = org.Pod + Location = org.Location + + Build = deploys.Build + CreateBuildRequest = deploys.CreateBuildRequest + Deploy = deploys.Deploy + CreateDeployRequest = deploys.CreateDeployRequest + UpdateDeployRequest = deploys.UpdateDeployRequest + DeployEnvItem = deploys.DeployEnvItem + DeployEnvVar = deploys.DeployEnvVar + DeploySecretRef = deploys.DeploySecretRef + TenancyType = deploys.TenancyType + Route = deploys.Route + CreateRouteRequest = deploys.CreateRouteRequest + CustomDomain = deploys.CustomDomain + + LogsResponse = logs.LogsResponse + LogEntry = logs.LogEntry + MetricsResponse = metrics.MetricsResponse + MetricSeries = metrics.MetricSeries + + // UUID is the type of resource IDs in requests and responses. + UUID = uuid.UUID +) + +// Client is every API call the CLI makes. +type Client interface { + ListOrganizations(ctx context.Context) ([]Organization, error) + GetOrganization(ctx context.Context, orgID string) (*Organization, error) + ListProjects(ctx context.Context, orgID string) ([]Project, error) + GetProject(ctx context.Context, projectID string) (*Project, error) + ListEnvironments(ctx context.Context, projectID string) ([]Environment, error) + GetEnvironment(ctx context.Context, envID string) (*Environment, error) + ListLocations(ctx context.Context) ([]Location, error) + + ListBuilds(ctx context.Context, projectID string) ([]Build, error) + GetBuild(ctx context.Context, buildID string) (*Build, error) + CreateBuild(ctx context.Context, projectID string, req *CreateBuildRequest) (*Build, error) + + ListDeploys(ctx context.Context, projectID, envID string) ([]Deploy, error) + GetDeploy(ctx context.Context, deployID string) (*Deploy, error) + CreateDeploy(ctx context.Context, projectID, envID string, req *CreateDeployRequest) (*Deploy, error) + // UpdateDeploy reads the deploy, applies mutate to a full update + // request built from it and writes it back; the API replaces the whole + // resource. + UpdateDeploy(ctx context.Context, deployID string, mutate func(*UpdateDeployRequest) error) (*Deploy, error) + DeleteDeploy(ctx context.Context, deployID string) error + + // ListSecrets returns up to limit secret keys (all when limit <= 0). + // Values are never returned. + ListSecrets(ctx context.Context, projectID, envID string, limit int) ([]EnvironmentSecret, error) + SetSecret(ctx context.Context, projectID, envID, name, value string) error + DeleteSecret(ctx context.Context, projectID, envID, name string) error + + // ListPods returns up to limit pods (all when limit <= 0) in scope. + ListPods(ctx context.Context, scope PodScope, limit int) ([]Pod, error) + GetPod(ctx context.Context, podID string) (*Pod, error) + + GetLogs(ctx context.Context, deployID string, q *LogsQuery) (*LogsResponse, error) + + ListMetricTypes(ctx context.Context) ([]string, error) + GetDeployMetrics(ctx context.Context, deployID string, q MetricsQuery) (*MetricsResponse, error) + GetPodMetrics(ctx context.Context, podName string, q MetricsQuery) (*MetricsResponse, error) + + ListRoutes(ctx context.Context, deployID string) ([]Route, error) + CreateRoute(ctx context.Context, deployID string, req CreateRouteRequest) (*Route, error) + DeleteRoute(ctx context.Context, deployID, routeID string) error + + ListDomains(ctx context.Context, orgID string) ([]CustomDomain, error) + CreateDomain(ctx context.Context, orgID, domain string) (*CustomDomain, error) + VerifyDomain(ctx context.Context, orgID, domain string) (*CustomDomain, error) + DeleteDomain(ctx context.Context, orgID, domain string) error +} + +// TokenSource supplies the bearer token for each request. It is asked on +// every attempt, so an implementation can refresh expired tokens. +type TokenSource interface { + Token(ctx context.Context) (string, error) +} + +// StaticToken is a TokenSource for a fixed token such as DEVLYFT_API_TOKEN. +type StaticToken string + +// Token returns the token. +func (t StaticToken) Token(context.Context) (string, error) { return string(t), nil } + +// Options configure a client. +type Options struct { + // BaseURL is the API origin, e.g. https://api.devlyft.io. + BaseURL string + // Tokens supplies the bearer token; nil sends no Authorization header. + Tokens TokenSource + // HTTPClient carries proxy and CA settings; nil uses a default client. + HTTPClient *http.Client + // Logger receives debug logs of each exchange, redacted; nil discards. + Logger *slog.Logger + // UserAgent defaults to DefaultUserAgent(). + UserAgent string + // Retry defaults to DefaultRetryPolicy. + Retry RetryPolicy +} + +// DefaultUserAgent identifies the CLI, its version and platform. +func DefaultUserAgent() string { + return fmt.Sprintf("devlyft/%s (%s/%s)", version.Get().Version, runtime.GOOS, runtime.GOARCH) +} + +// client implements Client over the generated clients. +type client struct { + baseURL string + doer *doer + deploys *deploys.ClientWithResponses + org *org.ClientWithResponses + logs *logs.ClientWithResponses + metrics *metrics.ClientWithResponses +} + +var _ Client = (*client)(nil) + +// New returns a client for the API at opts.BaseURL. Tokens are only ever +// sent over HTTPS, except to a loopback address. +func New(opts Options) (Client, error) { //nolint:gocritic // options are passed by value, like http.Server + u, err := url.Parse(opts.BaseURL) + if err != nil || u.Host == "" { + return nil, fmt.Errorf("invalid API URL %q", opts.BaseURL) + } + if u.Scheme != "https" && (u.Scheme != "http" || !isLoopback(u.Hostname())) { + return nil, fmt.Errorf("invalid API URL %q: must use https", opts.BaseURL) + } + if opts.HTTPClient == nil { + opts.HTTPClient = &http.Client{} + } + if opts.Logger == nil { + opts.Logger = slog.New(slog.DiscardHandler) + } + if opts.UserAgent == "" { + opts.UserAgent = DefaultUserAgent() + } + base := strings.TrimRight(opts.BaseURL, "/") + d := newDoer(&opts) + + c := &client{baseURL: base, doer: d} + if c.deploys, err = deploys.NewClientWithResponses(base, deploys.WithHTTPClient(d)); err != nil { + return nil, err + } + if c.org, err = org.NewClientWithResponses(base, org.WithHTTPClient(d)); err != nil { + return nil, err + } + if c.logs, err = logs.NewClientWithResponses(base, logs.WithHTTPClient(d)); err != nil { + return nil, err + } + if c.metrics, err = metrics.NewClientWithResponses(base, metrics.WithHTTPClient(d)); err != nil { + return nil, err + } + return c, nil +} + +func isLoopback(host string) bool { + if host == "localhost" { + return true + } + ip := net.ParseIP(host) + return ip != nil && ip.IsLoopback() +} + +// errNoScope is returned by ListPods without a scope. +var errNoScope = errors.New("pod scope needs a project, environment or deploy ID") diff --git a/internal/api/client_test.go b/internal/api/client_test.go new file mode 100644 index 0000000..bcde641 --- /dev/null +++ b/internal/api/client_test.go @@ -0,0 +1,208 @@ +package api + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/devlyft/cli/internal/clierr" + "github.com/devlyft/cli/internal/exitcode" +) + +func TestNewRejectsInsecureURLs(t *testing.T) { + t.Parallel() + + tests := []struct { + url string + ok bool + }{ + {"https://api.devlyft.io", true}, + {"https://api.devlyft.io/", true}, + {"http://127.0.0.1:8080", true}, + {"http://localhost:8080", true}, + {"http://[::1]:8080", true}, + {"http://api.devlyft.io", false}, + {"ftp://api.devlyft.io", false}, + {"api.devlyft.io", false}, + {"", false}, + } + for _, tt := range tests { + _, err := New(Options{BaseURL: tt.url}) + if (err == nil) != tt.ok { + t.Errorf("New(%q) err = %v, want ok=%v", tt.url, err, tt.ok) + } + } +} + +func TestInvalidIDIsUsageError(t *testing.T) { + t.Parallel() + + c, err := New(Options{BaseURL: "https://api.devlyft.io"}) + if err != nil { + t.Fatal(err) + } + _, err = c.GetDeploy(t.Context(), "web") + if clierr.ExitCode(err) != exitcode.Usage { + t.Errorf("exit = %d, want %d (err %v)", clierr.ExitCode(err), exitcode.Usage, err) + } +} + +func TestUpdateDeployReadModifyWrite(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + etag string + wantIfMatch string + }{ + {name: "with ETag sends If-Match", etag: `"v7"`, wantIfMatch: `"v7"`}, + {name: "without ETag sends no precondition"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var put *http.Request + var putBody map[string]any + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if r.Method == http.MethodPut { + put = r + _ = json.NewDecoder(r.Body).Decode(&putBody) + } + if tt.etag != "" { + w.Header().Set("ETag", tt.etag) + } + _, _ = w.Write([]byte(deployJSON)) + })) + defer srv.Close() + c, _ := newTestClient(t, srv, nil) + + _, err := c.UpdateDeploy(t.Context(), testDeployID, func(r *UpdateDeployRequest) error { + r.Replicas = 4 + return nil + }) + if err != nil { + t.Fatal(err) + } + if put == nil { + t.Fatal("no PUT sent") + } + if got := put.Header.Get("If-Match"); got != tt.wantIfMatch { + t.Errorf("If-Match = %q, want %q", got, tt.wantIfMatch) + } + // The server requires these on every update; unchanged fields + // must be carried over from the GET. + want := map[string]any{ + "build_id": "11111111-2222-4333-8444-555555555555", + "name": "web", + "location_id": "eu-west", + "replicas": float64(4), + "expose": true, + "tenancy_type": "INHERIT", + } + for k, v := range want { + if putBody[k] != v { + t.Errorf("PUT %s = %v, want %v", k, putBody[k], v) + } + } + if env, ok := putBody["env"].([]any); !ok || len(env) != 1 { + t.Errorf("PUT env = %v, want the existing entry", putBody["env"]) + } + }) + } +} + +func TestUpdateDeployMutateErrorStops(t *testing.T) { + t.Parallel() + + s := &scripted{body: deployJSON} + srv := httptest.NewServer(s) + defer srv.Close() + c, _ := newTestClient(t, srv, nil) + + boom := errors.New("bad flag") + _, err := c.UpdateDeploy(t.Context(), testDeployID, func(*UpdateDeployRequest) error { return boom }) + if !errors.Is(err, boom) { + t.Fatalf("err = %v, want %v", err, boom) + } + if len(s.requests) != 1 { + t.Errorf("requests = %d, want only the GET", len(s.requests)) + } +} + +func TestUpdateRequestFromDeployWithHPA(t *testing.T) { + t.Parallel() + + var d Deploy + if err := json.Unmarshal([]byte(deployJSON), &d); err != nil { + t.Fatal(err) + } + d.EnableHpa = true + minR, maxR := uint32(2), uint32(5) + d.HpaMinReplicas, d.HpaMaxReplicas = &minR, &maxR + + req := UpdateRequestFromDeploy(&d) + if req.Replicas != 0 { + t.Errorf("Replicas = %d, want 0 with autoscaling", req.Replicas) + } + if *req.HpaMinReplicas != 2 || *req.HpaMaxReplicas != 5 { + t.Errorf("HPA bounds not carried over: %+v", req) + } +} + +func TestErrorMapping(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + status int + header string + body string + wantCode clierr.Code + wantMsg string + wantReqID string + }{ + {"problem detail", 404, "", `{"title":"Not Found","detail":"deploy not found","request_id":"req-body"}`, clierr.CodeNotFound, "deploy not found", "req-body"}, + {"header wins over body", 409, "req-hdr", `{"message":"name taken","request_id":"req-body"}`, clierr.CodeConflict, "name taken", "req-hdr"}, + {"error field", 400, "", `{"error":"replicas must be at least 1"}`, clierr.CodeInvalidArgument, "replicas must be at least 1", ""}, + {"plain text", 403, "", "forbidden\n", clierr.CodePermissionDenied, "forbidden", ""}, + {"html is ignored", 401, "", "nope", clierr.CodeAuthRequired, "Unauthorized", ""}, + {"empty body", 412, "req-1", "", clierr.CodeConflict, "Precondition Failed", "req-1"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if tt.header != "" { + w.Header().Set("X-Request-Id", tt.header) + } + w.WriteHeader(tt.status) + _, _ = w.Write([]byte(tt.body)) + })) + defer srv.Close() + c, _ := newTestClient(t, srv, nil) + + err := getDeploy(context.Background(), c) //nolint:usetesting // parallel subtests outlive t.Context of the parent + e := clierr.As(err) + if e.Code != tt.wantCode || e.Message != tt.wantMsg || e.RequestID != tt.wantReqID || e.HTTPStatus != tt.status { + t.Errorf("got code=%s msg=%q reqID=%q status=%d", e.Code, e.Message, e.RequestID, e.HTTPStatus) + } + }) + } +} + +func TestListPodsNeedsScope(t *testing.T) { + t.Parallel() + + c, err := New(Options{BaseURL: "https://api.devlyft.io"}) + if err != nil { + t.Fatal(err) + } + _, err = c.ListPods(t.Context(), PodScope{}, 10) + if clierr.ExitCode(err) != exitcode.Usage { + t.Errorf("err = %v, want usage error", err) + } +} diff --git a/internal/api/contract_test.go b/internal/api/contract_test.go new file mode 100644 index 0000000..fde33ef --- /dev/null +++ b/internal/api/contract_test.go @@ -0,0 +1,259 @@ +package api + +import ( + "bytes" + "context" + "io" + "net/http" + "net/http/httptest" + "path/filepath" + "slices" + "strings" + "sync" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers" + "github.com/getkin/kin-openapi/routers/legacy" +) + +// Contract tests: every request the facade sends must match an operation +// in the vendored specs, and the canned responses must match them too, so a +// spec update that breaks the CLI fails here. + +const ( + uuidA = "0b4e1a3c-6c2f-4d0a-8c51-5f0f1e2d3c4b" + ts = `"2026-10-01T00:00:00Z"` +) + +var ( + orgJSON = `{"id":"` + uuidA + `","name":"Acme","slug":"acme","created_at":` + ts + `,"updated_at":` + ts + `}` + projectJSON = `{"id":"` + uuidA + `","organization_id":"` + uuidA + `","name":"web","created_at":` + ts + `,"updated_at":` + ts + `}` + envJSON = `{"id":"` + uuidA + `","project_id":"` + uuidA + `","name":"production","tenancy_type":"SHARED","created_at":` + ts + `,"updated_at":` + ts + `}` + podJSON = `{"id":"p-1","name":"web-abc","phase":"Running","location_id":"eu-west","cluster_name":"edge-1","project_id":"` + uuidA + `","environment_id":"` + uuidA + `","deploy_id":"` + uuidA + `"}` + buildJSON = `{"id":"` + uuidA + `","project_id":"` + uuidA + `","name":"web-1.4.2","image":"ghcr.io/acme/web","tag":"1.4.2","cpu_request":"250m","memory_request":"512Mi","container_port":8080,"created_at":` + ts + `,"updated_at":` + ts + `}` + routeJSON = `{"id":"` + uuidA + `","deploy_id":"` + uuidA + `","hostname":"www.acme.dev","path_prefix":"/","created_at":` + ts + `,"updated_at":` + ts + `}` + domainJSON = `{"id":"` + uuidA + `","organization_id":"` + uuidA + `","domain":"acme.dev","status":"pending","verification_token":"t","challenge_record_name":"_devlyft-challenge.acme.dev","verified_at":null,"last_checked_at":null,"created_at":` + ts + `,"updated_at":` + ts + `}` + metricsJSON = `{"metric":"cpu","series":[{"labels":{"pod":"web-abc"},"points":[{"timestamp":` + ts + `,"value":0.5}]}]}` +) + +// responses are canned bodies by operation ID. +// +//nolint:gosec // fixtures, not credentials +var responses = map[string]string{ + "GetV1Organizations": `{"organizations":[` + orgJSON + `]}`, + "GetV1OrganizationsOrganizationId": orgJSON, + "GetV1OrganizationsOrganizationIdProjects": `{"projects":[` + projectJSON + `]}`, + "GetV1ProjectsProjectId": projectJSON, + "GetV1ProjectsProjectIdEnvironments": `{"environments":[` + envJSON + `]}`, + "GetV1EnvironmentsEnvironmentId": envJSON, + "GetV1Locations": `{"locations":[{"id":"eu-west"}]}`, + "ListEnvironmentSecretsV1": `{"secrets":[{"key":"DB_URL"}],"limit":25,"offset":0,"total":1}`, + "CreateEnvironmentSecretV1": `{"key":"DB_URL"}`, + "DeleteEnvironmentSecretV1": ``, + "GetV1ProjectsProjectIdPods": `{"results":[` + podJSON + `],"limit":25,"offset":0,"total":1}`, + "GetV1EnvironmentsEnvironmentIdPods": `{"results":[` + podJSON + `],"limit":25,"offset":0,"total":1}`, + "GetV1DeploysDeployIdPods": `{"results":[` + podJSON + `],"limit":25,"offset":0,"total":1}`, + "GetV1PodsPodId": podJSON, + "GetV1ProjectsProjectIdBuilds": `{"builds":[` + buildJSON + `]}`, + "GetV1BuildsBuildId": buildJSON, + "PostV1ProjectsProjectIdBuilds": buildJSON, + "GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys": `{"deploys":[` + deployJSON + `]}`, + "GetV1DeploysDeployId": deployJSON, + "PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploys": deployJSON, + "PutV1DeploysDeployId": deployJSON, + "DeleteV1DeploysDeployId": ``, + "GetV1DeploysDeployIdRoutes": `{"routes":[` + routeJSON + `]}`, + "PostV1DeploysDeployIdRoutes": routeJSON, + "DeleteV1DeploysDeployIdRoutesRouteId": ``, + "GetV1OrganizationsOrganizationIdDomains": `{"domains":[` + domainJSON + `]}`, + "PostV1OrganizationsOrganizationIdDomains": domainJSON, + "PostV1OrganizationsOrganizationIdDomainsDomainVerify": domainJSON, + "DeleteV1OrganizationsOrganizationIdDomainsDomain": ``, + "GetV1DeploysDeployIdLogs": `{"entries":[{"timestamp":` + ts + `,"line":"hello","pod":"web-abc"}],"cursor":` + ts + `}`, + "GetV1MetricTypes": `{"metrics":["cpu","memory"]}`, + "GetV1DeploysDeployIdMetrics": metricsJSON, + "GetV1PodsPodIdMetrics": metricsJSON, +} + +// contractServer validates every exchange against the vendored specs. +type contractServer struct { + t *testing.T + routers []routers.Router + mu sync.Mutex + seen []string +} + +func newContractServer(t *testing.T) (*contractServer, *httptest.Server) { + t.Helper() + cs := &contractServer{t: t} + srv := httptest.NewServer(cs) + t.Cleanup(srv.Close) + + for _, name := range []string{"deploys", "org", "logs", "metrics"} { + loader := openapi3.NewLoader() + doc, err := loader.LoadFromFile(filepath.Join("specs", name+".yaml")) + if err != nil { + t.Fatalf("loading %s spec: %v", name, err) + } + if err := doc.Validate(loader.Context); err != nil { + t.Fatalf("%s spec is invalid: %v", name, err) + } + doc.Servers = openapi3.Servers{{URL: srv.URL}} + r, err := legacy.NewRouter(doc) + if err != nil { + t.Fatal(err) + } + cs.routers = append(cs.routers, r) + } + return cs, srv +} + +func (cs *contractServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { + // Server-side URLs have no scheme or host; the router matches them + // against the spec's server URL. + req.URL.Scheme, req.URL.Host = "http", req.Host + var route *routers.Route + var params map[string]string + for _, r := range cs.routers { + if rt, p, err := r.FindRoute(req); err == nil { + route, params = rt, p + break + } + } + if route == nil { + cs.t.Errorf("%s %s matches no vendored operation", req.Method, req.URL.Path) + http.Error(w, "no route", http.StatusNotFound) + return + } + id := route.Operation.OperationID + cs.mu.Lock() + cs.seen = append(cs.seen, id) + cs.mu.Unlock() + + ctx := context.Background() //nolint:usetesting // validation outlives no test + in := &openapi3filter.RequestValidationInput{ + Request: req, + PathParams: params, + Route: route, + Options: &openapi3filter.Options{MultiError: true}, + } + if err := openapi3filter.ValidateRequest(ctx, in); err != nil { + cs.t.Errorf("%s: request does not match the spec: %v", id, err) + } + + body := responses[id] + status := successStatus(route.Operation) + out := &openapi3filter.ResponseValidationInput{ + RequestValidationInput: in, + Status: status, + Header: http.Header{"Content-Type": {"application/json"}}, + Body: io.NopCloser(strings.NewReader(body)), + Options: &openapi3filter.Options{MultiError: true, IncludeResponseStatus: true}, + } + if err := openapi3filter.ValidateResponse(ctx, out); err != nil { + cs.t.Errorf("%s: canned response does not match the spec: %v", id, err) + } + + if body != "" { + w.Header().Set("Content-Type", "application/json") + } + w.WriteHeader(status) + _, _ = io.Copy(w, bytes.NewBufferString(body)) +} + +// successStatus is the operation's 2xx response code. +func successStatus(op *openapi3.Operation) int { + for _, code := range []int{200, 201, 204} { + if op.Responses.Status(code) != nil { + return code + } + } + return 200 +} + +func TestContract(t *testing.T) { + t.Parallel() + + cs, srv := newContractServer(t) + c, err := New(Options{BaseURL: srv.URL, Tokens: StaticToken("t"), HTTPClient: srv.Client()}) + if err != nil { + t.Fatal(err) + } + ctx := t.Context() + check := func(name string, err error) { + t.Helper() + if err != nil { + t.Errorf("%s: %v", name, err) + } + } + second := func(_ any, err error) error { return err } + + check("ListOrganizations", second(c.ListOrganizations(ctx))) + check("GetOrganization", second(c.GetOrganization(ctx, uuidA))) + check("ListProjects", second(c.ListProjects(ctx, uuidA))) + check("GetProject", second(c.GetProject(ctx, uuidA))) + check("ListEnvironments", second(c.ListEnvironments(ctx, uuidA))) + check("GetEnvironment", second(c.GetEnvironment(ctx, uuidA))) + check("ListLocations", second(c.ListLocations(ctx))) + + check("ListSecrets", second(c.ListSecrets(ctx, uuidA, uuidA, 10))) + check("SetSecret", c.SetSecret(ctx, uuidA, uuidA, "DB_URL", "postgres://x")) + check("DeleteSecret", c.DeleteSecret(ctx, uuidA, uuidA, "DB_URL")) + + check("ListPods(project)", second(c.ListPods(ctx, PodScope{ProjectID: uuidA}, 0))) + check("ListPods(env)", second(c.ListPods(ctx, PodScope{EnvironmentID: uuidA}, 5))) + check("ListPods(deploy)", second(c.ListPods(ctx, PodScope{DeployID: uuidA}, 5))) + check("GetPod", second(c.GetPod(ctx, "p-1"))) + + check("ListBuilds", second(c.ListBuilds(ctx, uuidA))) + check("GetBuild", second(c.GetBuild(ctx, uuidA))) + port := 8080 + check("CreateBuild", second(c.CreateBuild(ctx, uuidA, &CreateBuildRequest{ + Name: "web-1.4.2", Image: "ghcr.io/acme/web", Tag: "1.4.2", CpuRequest: "250m", MemoryRequest: "512Mi", ContainerPort: &port, + }))) + + check("ListDeploys", second(c.ListDeploys(ctx, uuidA, uuidA))) + check("GetDeploy", second(c.GetDeploy(ctx, testDeployID))) + envItems := []DeployEnvItem{EnvVarItem("LOG_LEVEL", "info"), SecretRefItem("DB_URL")} + check("CreateDeploy", second(c.CreateDeploy(ctx, uuidA, uuidA, &CreateDeployRequest{ + BuildId: uuidMust(uuidA), Name: "web", LocationId: "eu-west", Replicas: 2, Env: &envItems, + }))) + check("UpdateDeploy", second(c.UpdateDeploy(ctx, testDeployID, func(r *UpdateDeployRequest) error { + r.Replicas = 3 + return nil + }))) + check("DeleteDeploy", c.DeleteDeploy(ctx, uuidA)) + + check("ListRoutes", second(c.ListRoutes(ctx, uuidA))) + prefix := "/api" + check("CreateRoute", second(c.CreateRoute(ctx, uuidA, CreateRouteRequest{Hostname: "www.acme.dev", PathPrefix: &prefix}))) + check("DeleteRoute", c.DeleteRoute(ctx, uuidA, uuidA)) + + check("ListDomains", second(c.ListDomains(ctx, uuidA))) + check("CreateDomain", second(c.CreateDomain(ctx, uuidA, "acme.dev"))) + check("VerifyDomain", second(c.VerifyDomain(ctx, uuidA, "acme.dev"))) + check("DeleteDomain", c.DeleteDomain(ctx, uuidA, "acme.dev")) + + check("GetLogs", second(c.GetLogs(ctx, uuidA, &LogsQuery{Pod: "web-abc", Since: "1h", Filter: "err", Match: []string{"status=500", "path=~/api"}, Limit: 100}))) + check("ListMetricTypes", second(c.ListMetricTypes(ctx))) + check("GetDeployMetrics", second(c.GetDeployMetrics(ctx, uuidA, MetricsQuery{Metric: "cpu", Pod: "web-abc", Range: "1h", Step: "30s"}))) + check("GetPodMetrics", second(c.GetPodMetrics(ctx, "web-abc", MetricsQuery{Metric: "memory"}))) + + // Every vendored operation is exercised, so the allowlist carries + // nothing the CLI does not use. + for id := range responses { + if !slices.Contains(cs.seen, id) { + t.Errorf("operation %s was never called", id) + } + } +} + +func uuidMust(s string) UUID { + u, err := parseID("test", s) + if err != nil { + panic(err) + } + return u +} diff --git a/internal/api/deploys.go b/internal/api/deploys.go new file mode 100644 index 0000000..d9aabb1 --- /dev/null +++ b/internal/api/deploys.go @@ -0,0 +1,290 @@ +package api + +import ( + "context" + "net/http" + + "github.com/devlyft/cli/internal/api/gen/deploys" +) + +// httpResponse shortens signatures of helpers over generated responses. +type httpResponse = http.Response + +// Builds, deploys, routes and custom domains are served by deploys-api. + +func (c *client) ListBuilds(ctx context.Context, projectID string) ([]Build, error) { + id, err := parseID("project", projectID) + if err != nil { + return nil, err + } + ctx, n := withAttempts(ctx) + r, err := c.deploys.GetV1ProjectsProjectIdBuildsWithResponse(ctx, id) + if err != nil { + return nil, c.transportError(err) + } + v, err := expect(r.HTTPResponse, r.Body, r.JSON200, *n) + if err != nil { + return nil, err + } + return v.Builds, nil +} + +func (c *client) GetBuild(ctx context.Context, buildID string) (*Build, error) { + id, err := parseID("build", buildID) + if err != nil { + return nil, err + } + ctx, n := withAttempts(ctx) + r, err := c.deploys.GetV1BuildsBuildIdWithResponse(ctx, id) + if err != nil { + return nil, c.transportError(err) + } + return expect(r.HTTPResponse, r.Body, r.JSON200, *n) +} + +func (c *client) CreateBuild(ctx context.Context, projectID string, req *CreateBuildRequest) (*Build, error) { + id, err := parseID("project", projectID) + if err != nil { + return nil, err + } + ctx, n := withAttempts(ctx) + r, err := c.deploys.PostV1ProjectsProjectIdBuildsWithResponse(ctx, id, deploys.PostV1ProjectsProjectIdBuildsJSONRequestBody(*req)) + if err != nil { + return nil, c.transportError(err) + } + return expect(r.HTTPResponse, r.Body, r.JSON201, *n) +} + +func (c *client) ListDeploys(ctx context.Context, projectID, envID string) ([]Deploy, error) { + pid, err := parseID("project", projectID) + if err != nil { + return nil, err + } + eid, err := parseID("environment", envID) + if err != nil { + return nil, err + } + ctx, n := withAttempts(ctx) + r, err := c.deploys.GetV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithResponse(ctx, pid, eid) + if err != nil { + return nil, c.transportError(err) + } + v, err := expect(r.HTTPResponse, r.Body, r.JSON200, *n) + if err != nil { + return nil, err + } + return v.Deploys, nil +} + +func (c *client) GetDeploy(ctx context.Context, deployID string) (*Deploy, error) { + d, _, err := c.getDeploy(ctx, deployID) + return d, err +} + +// getDeploy also returns the response's ETag, if the server sent one. +func (c *client) getDeploy(ctx context.Context, deployID string) (*Deploy, string, error) { + id, err := parseID("deploy", deployID) + if err != nil { + return nil, "", err + } + ctx, n := withAttempts(ctx) + r, err := c.deploys.GetV1DeploysDeployIdWithResponse(ctx, id) + if err != nil { + return nil, "", c.transportError(err) + } + d, err := expect(r.HTTPResponse, r.Body, r.JSON200, *n) + if err != nil { + return nil, "", err + } + return d, r.HTTPResponse.Header.Get("ETag"), nil +} + +func (c *client) CreateDeploy(ctx context.Context, projectID, envID string, req *CreateDeployRequest) (*Deploy, error) { + pid, err := parseID("project", projectID) + if err != nil { + return nil, err + } + eid, err := parseID("environment", envID) + if err != nil { + return nil, err + } + ctx, n := withAttempts(ctx) + r, err := c.deploys.PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysWithResponse(ctx, pid, eid, deploys.PostV1ProjectsProjectIdEnvironmentsEnvironmentIdDeploysJSONRequestBody(*req)) + if err != nil { + return nil, c.transportError(err) + } + return expect(r.HTTPResponse, r.Body, r.JSON201, *n) +} + +// UpdateDeploy is a read-modify-write: the API replaces the whole deploy +// and requires build_id, name and location_id on every update. When the +// GET returns an ETag the PUT sends it as If-Match, so a concurrent change +// fails with a conflict instead of being overwritten. +func (c *client) UpdateDeploy(ctx context.Context, deployID string, mutate func(*UpdateDeployRequest) error) (*Deploy, error) { + cur, etag, err := c.getDeploy(ctx, deployID) + if err != nil { + return nil, err + } + req := UpdateRequestFromDeploy(cur) + if err := mutate(&req); err != nil { + return nil, err + } + var editors []deploys.RequestEditorFn + if etag != "" { + editors = append(editors, func(_ context.Context, r *http.Request) error { + r.Header.Set("If-Match", etag) + return nil + }) + } + ctx, n := withAttempts(ctx) + r, err := c.deploys.PutV1DeploysDeployIdWithResponse(ctx, cur.Id, deploys.PutV1DeploysDeployIdJSONRequestBody(req), editors...) + if err != nil { + return nil, c.transportError(err) + } + return expect(r.HTTPResponse, r.Body, r.JSON200, *n) +} + +// UpdateRequestFromDeploy returns the update request that leaves d as it +// is. `deploy --dry-run` diffs against it. +func UpdateRequestFromDeploy(d *Deploy) UpdateDeployRequest { + req := UpdateDeployRequest{ + BuildId: d.BuildId, + Name: d.Name, + LocationId: d.LocationId, + Env: d.Env, + EnableHpa: d.EnableHpa, + Replicas: d.Replicas, + HpaMinReplicas: d.HpaMinReplicas, + HpaMaxReplicas: d.HpaMaxReplicas, + HpaTargetCpu: d.HpaTargetCpu, + InstanceType: d.InstanceType, + RegistryCredentialName: d.RegistryCredentialName, + } + expose := d.Expose + req.Expose = &expose + if d.TenancyType != "" { + tenancy := d.TenancyType + req.TenancyType = &tenancy + } + if d.EnableHpa { + // The API rejects replicas alongside autoscaling. + req.Replicas = 0 + } + return req +} + +func (c *client) DeleteDeploy(ctx context.Context, deployID string) error { + id, err := parseID("deploy", deployID) + if err != nil { + return err + } + ctx, n := withAttempts(ctx) + r, err := c.deploys.DeleteV1DeploysDeployIdWithResponse(ctx, id) + if err != nil { + return c.transportError(err) + } + return checkStatus(r.HTTPResponse, r.Body, *n) +} + +func (c *client) ListRoutes(ctx context.Context, deployID string) ([]Route, error) { + id, err := parseID("deploy", deployID) + if err != nil { + return nil, err + } + ctx, n := withAttempts(ctx) + r, err := c.deploys.GetV1DeploysDeployIdRoutesWithResponse(ctx, id) + if err != nil { + return nil, c.transportError(err) + } + v, err := expect(r.HTTPResponse, r.Body, r.JSON200, *n) + if err != nil { + return nil, err + } + return v.Routes, nil +} + +func (c *client) CreateRoute(ctx context.Context, deployID string, req CreateRouteRequest) (*Route, error) { + id, err := parseID("deploy", deployID) + if err != nil { + return nil, err + } + ctx, n := withAttempts(ctx) + r, err := c.deploys.PostV1DeploysDeployIdRoutesWithResponse(ctx, id, deploys.PostV1DeploysDeployIdRoutesJSONRequestBody(req)) + if err != nil { + return nil, c.transportError(err) + } + return expect(r.HTTPResponse, r.Body, r.JSON201, *n) +} + +func (c *client) DeleteRoute(ctx context.Context, deployID, routeID string) error { + did, err := parseID("deploy", deployID) + if err != nil { + return err + } + rid, err := parseID("route", routeID) + if err != nil { + return err + } + ctx, n := withAttempts(ctx) + r, err := c.deploys.DeleteV1DeploysDeployIdRoutesRouteIdWithResponse(ctx, did, rid) + if err != nil { + return c.transportError(err) + } + return checkStatus(r.HTTPResponse, r.Body, *n) +} + +func (c *client) ListDomains(ctx context.Context, orgID string) ([]CustomDomain, error) { + id, err := parseID("organization", orgID) + if err != nil { + return nil, err + } + ctx, n := withAttempts(ctx) + r, err := c.deploys.GetV1OrganizationsOrganizationIdDomainsWithResponse(ctx, id) + if err != nil { + return nil, c.transportError(err) + } + v, err := expect(r.HTTPResponse, r.Body, r.JSON200, *n) + if err != nil { + return nil, err + } + return v.Domains, nil +} + +func (c *client) CreateDomain(ctx context.Context, orgID, domain string) (*CustomDomain, error) { + id, err := parseID("organization", orgID) + if err != nil { + return nil, err + } + ctx, n := withAttempts(ctx) + r, err := c.deploys.PostV1OrganizationsOrganizationIdDomainsWithResponse(ctx, id, deploys.PostV1OrganizationsOrganizationIdDomainsJSONRequestBody{Domain: domain}) + if err != nil { + return nil, c.transportError(err) + } + return expect(r.HTTPResponse, r.Body, r.JSON201, *n) +} + +func (c *client) VerifyDomain(ctx context.Context, orgID, domain string) (*CustomDomain, error) { + id, err := parseID("organization", orgID) + if err != nil { + return nil, err + } + ctx, n := withAttempts(ctx) + r, err := c.deploys.PostV1OrganizationsOrganizationIdDomainsDomainVerifyWithResponse(ctx, id, domain) + if err != nil { + return nil, c.transportError(err) + } + return expect(r.HTTPResponse, r.Body, r.JSON200, *n) +} + +func (c *client) DeleteDomain(ctx context.Context, orgID, domain string) error { + id, err := parseID("organization", orgID) + if err != nil { + return err + } + ctx, n := withAttempts(ctx) + r, err := c.deploys.DeleteV1OrganizationsOrganizationIdDomainsDomainWithResponse(ctx, id, domain) + if err != nil { + return c.transportError(err) + } + return checkStatus(r.HTTPResponse, r.Body, *n) +} diff --git a/internal/api/envitem.go b/internal/api/envitem.go new file mode 100644 index 0000000..4e973e3 --- /dev/null +++ b/internal/api/envitem.go @@ -0,0 +1,55 @@ +package api + +import ( + "encoding/json" + "fmt" + + "github.com/devlyft/cli/internal/api/gen/deploys" +) + +// The platform spec's DeployEnvItem discriminator has no mapping, so the +// generated FromDeployEnvVar, FromDeploySecretRef and ValueByDiscriminator +// use schema names ("DeployEnvVar") as the kind instead of "env" and +// "secret", which the API rejects. Build and read env items with these +// helpers instead. + +// EnvVarItem returns an env entry with a plain value. +func EnvVarItem(key, value string) DeployEnvItem { + return envItem(DeployEnvVar{Kind: deploys.Env, Key: key, Value: value}) +} + +// SecretRefItem returns an env entry that takes the value of the +// environment secret named key; the variable's name equals the key. +func SecretRefItem(key string) DeployEnvItem { + return envItem(DeploySecretRef{Kind: deploys.Secret, Key: key}) +} + +func envItem(v any) DeployEnvItem { + b, err := json.Marshal(v) + if err != nil { + panic(err) // plain structs of strings always marshal + } + var item DeployEnvItem + if err := item.UnmarshalJSON(b); err != nil { + panic(err) + } + return item +} + +// EnvItemValue returns the env entry as exactly one of a plain variable or +// a secret reference. +func EnvItemValue(item DeployEnvItem) (*DeployEnvVar, *DeploySecretRef, error) { + kind, err := item.Discriminator() + if err != nil { + return nil, nil, err + } + switch kind { + case string(deploys.Env): + v, err := item.AsDeployEnvVar() + return &v, nil, err + case string(deploys.Secret): + s, err := item.AsDeploySecretRef() + return nil, &s, err + } + return nil, nil, fmt.Errorf("unknown env entry kind %q", kind) +} diff --git a/internal/api/envitem_test.go b/internal/api/envitem_test.go new file mode 100644 index 0000000..1bb36fb --- /dev/null +++ b/internal/api/envitem_test.go @@ -0,0 +1,70 @@ +package api + +import ( + "encoding/json" + "testing" +) + +func TestEnvItems(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + item DeployEnvItem + want string + }{ + {"plain", EnvVarItem("LOG_LEVEL", "info"), `{"key":"LOG_LEVEL","kind":"env","value":"info"}`}, + {"secret", SecretRefItem("DB_URL"), `{"key":"DB_URL","kind":"secret"}`}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + b, err := json.Marshal(tt.item) + if err != nil { + t.Fatal(err) + } + if string(b) != tt.want { + t.Errorf("got %s, want %s", b, tt.want) + } + }) + } +} + +func TestEnvItemValue(t *testing.T) { + t.Parallel() + + v, s, err := EnvItemValue(EnvVarItem("A", "1")) + if err != nil || v == nil || s != nil || v.Key != "A" || v.Value != "1" { + t.Errorf("env var: %+v %+v %v", v, s, err) + } + v, s, err = EnvItemValue(SecretRefItem("B")) + if err != nil || v != nil || s == nil || s.Key != "B" { + t.Errorf("secret ref: %+v %+v %v", v, s, err) + } + var unknown DeployEnvItem + if err := unknown.UnmarshalJSON([]byte(`{"kind":"file","key":"C"}`)); err != nil { + t.Fatal(err) + } + if _, _, err := EnvItemValue(unknown); err == nil { + t.Error("unknown kind should fail") + } +} + +// The generated helper writes the schema name as the kind. If this starts +// failing, the platform spec gained a discriminator mapping and the +// helpers in envitem.go can go. +func TestGeneratedDiscriminatorStillBroken(t *testing.T) { + t.Parallel() + + var item DeployEnvItem + if err := item.FromDeployEnvVar(DeployEnvVar{Key: "A", Value: "1"}); err != nil { + t.Fatal(err) + } + kind, err := item.Discriminator() + if err != nil { + t.Fatal(err) + } + if kind != "DeployEnvVar" { + t.Errorf("generated kind = %q; the spec may now have a mapping, see envitem.go", kind) + } +} diff --git a/internal/api/errors.go b/internal/api/errors.go new file mode 100644 index 0000000..5da5e99 --- /dev/null +++ b/internal/api/errors.go @@ -0,0 +1,103 @@ +package api + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "unicode/utf8" + + "github.com/google/uuid" + + "github.com/devlyft/cli/internal/clierr" +) + +// expect returns v for a 2xx response, and the API error otherwise. +func expect[T any](resp *http.Response, body []byte, v *T, attempts int) (*T, error) { + if err := checkStatus(resp, body, attempts); err != nil { + return nil, err + } + if v == nil { + return nil, clierr.New(clierr.CodeInternal, "unexpected %s response from the API", resp.Status) + } + return v, nil +} + +// checkStatus returns nil for a 2xx response and an API error otherwise. +func checkStatus(resp *http.Response, body []byte, attempts int) error { + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + return nil + } + return apiError(resp, body, attempts) +} + +// apiError maps a non-2xx response to a *clierr.Error carrying the server's +// message and request ID. +func apiError(resp *http.Response, body []byte, attempts int) *clierr.Error { + msg, requestID := parseErrorBody(body) + if h := resp.Header.Get("X-Request-Id"); h != "" { + requestID = h + } + e := clierr.FromHTTP(resp.StatusCode, msg, requestID) + if attempts > 1 { + e.Message += fmt.Sprintf(" (after %d attempts)", attempts) + e.Details = map[string]any{"attempts": attempts} + } + return e +} + +// parseErrorBody extracts a message and request ID from an error body. The +// platform's services use a few shapes (RFC 7807 problem details, {error}, +// {message}); a short plain-text body is used as is. +func parseErrorBody(body []byte) (msg, requestID string) { + var fields map[string]any + if json.Unmarshal(body, &fields) == nil { + for _, k := range []string{"detail", "message", "error", "title"} { + if s, ok := fields[k].(string); ok && s != "" { + msg = s + break + } + } + for _, k := range []string{"request_id", "requestId"} { + if s, ok := fields[k].(string); ok && s != "" { + requestID = s + break + } + } + return msg, requestID + } + text := strings.TrimSpace(string(body)) + if text != "" && len(text) <= 300 && utf8.ValidString(text) && !strings.HasPrefix(text, "<") { + msg = text + } + return msg, "" +} + +// transportError wraps a failure to get any response. Cancellation and +// deadlines pass through unchanged so the caller sees an interrupt or a +// timeout. +func (c *client) transportError(err error) error { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return err + } + if _, ok := errors.AsType[*clierr.Error](err); ok { + return err + } + return &clierr.Error{ + Code: clierr.CodeUnavailable, + Message: fmt.Sprintf("cannot reach the DevLyft API at %s: %v", c.baseURL, err), + Retryable: true, + Err: err, + } +} + +// parseID parses a resource ID, reporting a usage error for a bad one. +func parseID(kind, id string) (uuid.UUID, error) { + u, err := uuid.Parse(id) + if err != nil { + return uuid.UUID{}, clierr.Usagef("invalid %s ID %q: expected a UUID", kind, id) + } + return u, nil +} diff --git a/internal/api/mock/client.go b/internal/api/mock/client.go new file mode 100644 index 0000000..5db8ab6 --- /dev/null +++ b/internal/api/mock/client.go @@ -0,0 +1,541 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: client.go +// +// Generated by this command: +// +// mockgen -source=client.go -destination=mock/client.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + api "github.com/devlyft/cli/internal/api" + gomock "go.uber.org/mock/gomock" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder + isgomock struct{} +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// CreateBuild mocks base method. +func (m *MockClient) CreateBuild(ctx context.Context, projectID string, req *api.CreateBuildRequest) (*api.Build, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateBuild", ctx, projectID, req) + ret0, _ := ret[0].(*api.Build) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateBuild indicates an expected call of CreateBuild. +func (mr *MockClientMockRecorder) CreateBuild(ctx, projectID, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBuild", reflect.TypeOf((*MockClient)(nil).CreateBuild), ctx, projectID, req) +} + +// CreateDeploy mocks base method. +func (m *MockClient) CreateDeploy(ctx context.Context, projectID, envID string, req *api.CreateDeployRequest) (*api.Deploy, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateDeploy", ctx, projectID, envID, req) + ret0, _ := ret[0].(*api.Deploy) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateDeploy indicates an expected call of CreateDeploy. +func (mr *MockClientMockRecorder) CreateDeploy(ctx, projectID, envID, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDeploy", reflect.TypeOf((*MockClient)(nil).CreateDeploy), ctx, projectID, envID, req) +} + +// CreateDomain mocks base method. +func (m *MockClient) CreateDomain(ctx context.Context, orgID, domain string) (*api.CustomDomain, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateDomain", ctx, orgID, domain) + ret0, _ := ret[0].(*api.CustomDomain) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateDomain indicates an expected call of CreateDomain. +func (mr *MockClientMockRecorder) CreateDomain(ctx, orgID, domain any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDomain", reflect.TypeOf((*MockClient)(nil).CreateDomain), ctx, orgID, domain) +} + +// CreateRoute mocks base method. +func (m *MockClient) CreateRoute(ctx context.Context, deployID string, req api.CreateRouteRequest) (*api.Route, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateRoute", ctx, deployID, req) + ret0, _ := ret[0].(*api.Route) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRoute indicates an expected call of CreateRoute. +func (mr *MockClientMockRecorder) CreateRoute(ctx, deployID, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRoute", reflect.TypeOf((*MockClient)(nil).CreateRoute), ctx, deployID, req) +} + +// DeleteDeploy mocks base method. +func (m *MockClient) DeleteDeploy(ctx context.Context, deployID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteDeploy", ctx, deployID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteDeploy indicates an expected call of DeleteDeploy. +func (mr *MockClientMockRecorder) DeleteDeploy(ctx, deployID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDeploy", reflect.TypeOf((*MockClient)(nil).DeleteDeploy), ctx, deployID) +} + +// DeleteDomain mocks base method. +func (m *MockClient) DeleteDomain(ctx context.Context, orgID, domain string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteDomain", ctx, orgID, domain) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteDomain indicates an expected call of DeleteDomain. +func (mr *MockClientMockRecorder) DeleteDomain(ctx, orgID, domain any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDomain", reflect.TypeOf((*MockClient)(nil).DeleteDomain), ctx, orgID, domain) +} + +// DeleteRoute mocks base method. +func (m *MockClient) DeleteRoute(ctx context.Context, deployID, routeID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteRoute", ctx, deployID, routeID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteRoute indicates an expected call of DeleteRoute. +func (mr *MockClientMockRecorder) DeleteRoute(ctx, deployID, routeID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRoute", reflect.TypeOf((*MockClient)(nil).DeleteRoute), ctx, deployID, routeID) +} + +// DeleteSecret mocks base method. +func (m *MockClient) DeleteSecret(ctx context.Context, projectID, envID, name string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteSecret", ctx, projectID, envID, name) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteSecret indicates an expected call of DeleteSecret. +func (mr *MockClientMockRecorder) DeleteSecret(ctx, projectID, envID, name any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSecret", reflect.TypeOf((*MockClient)(nil).DeleteSecret), ctx, projectID, envID, name) +} + +// GetBuild mocks base method. +func (m *MockClient) GetBuild(ctx context.Context, buildID string) (*api.Build, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBuild", ctx, buildID) + ret0, _ := ret[0].(*api.Build) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBuild indicates an expected call of GetBuild. +func (mr *MockClientMockRecorder) GetBuild(ctx, buildID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBuild", reflect.TypeOf((*MockClient)(nil).GetBuild), ctx, buildID) +} + +// GetDeploy mocks base method. +func (m *MockClient) GetDeploy(ctx context.Context, deployID string) (*api.Deploy, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDeploy", ctx, deployID) + ret0, _ := ret[0].(*api.Deploy) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDeploy indicates an expected call of GetDeploy. +func (mr *MockClientMockRecorder) GetDeploy(ctx, deployID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeploy", reflect.TypeOf((*MockClient)(nil).GetDeploy), ctx, deployID) +} + +// GetDeployMetrics mocks base method. +func (m *MockClient) GetDeployMetrics(ctx context.Context, deployID string, q api.MetricsQuery) (*api.MetricsResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDeployMetrics", ctx, deployID, q) + ret0, _ := ret[0].(*api.MetricsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDeployMetrics indicates an expected call of GetDeployMetrics. +func (mr *MockClientMockRecorder) GetDeployMetrics(ctx, deployID, q any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeployMetrics", reflect.TypeOf((*MockClient)(nil).GetDeployMetrics), ctx, deployID, q) +} + +// GetEnvironment mocks base method. +func (m *MockClient) GetEnvironment(ctx context.Context, envID string) (*api.Environment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEnvironment", ctx, envID) + ret0, _ := ret[0].(*api.Environment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEnvironment indicates an expected call of GetEnvironment. +func (mr *MockClientMockRecorder) GetEnvironment(ctx, envID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEnvironment", reflect.TypeOf((*MockClient)(nil).GetEnvironment), ctx, envID) +} + +// GetLogs mocks base method. +func (m *MockClient) GetLogs(ctx context.Context, deployID string, q *api.LogsQuery) (*api.LogsResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLogs", ctx, deployID, q) + ret0, _ := ret[0].(*api.LogsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLogs indicates an expected call of GetLogs. +func (mr *MockClientMockRecorder) GetLogs(ctx, deployID, q any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogs", reflect.TypeOf((*MockClient)(nil).GetLogs), ctx, deployID, q) +} + +// GetOrganization mocks base method. +func (m *MockClient) GetOrganization(ctx context.Context, orgID string) (*api.Organization, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOrganization", ctx, orgID) + ret0, _ := ret[0].(*api.Organization) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOrganization indicates an expected call of GetOrganization. +func (mr *MockClientMockRecorder) GetOrganization(ctx, orgID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganization", reflect.TypeOf((*MockClient)(nil).GetOrganization), ctx, orgID) +} + +// GetPod mocks base method. +func (m *MockClient) GetPod(ctx context.Context, podID string) (*api.Pod, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPod", ctx, podID) + ret0, _ := ret[0].(*api.Pod) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPod indicates an expected call of GetPod. +func (mr *MockClientMockRecorder) GetPod(ctx, podID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPod", reflect.TypeOf((*MockClient)(nil).GetPod), ctx, podID) +} + +// GetPodMetrics mocks base method. +func (m *MockClient) GetPodMetrics(ctx context.Context, podName string, q api.MetricsQuery) (*api.MetricsResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPodMetrics", ctx, podName, q) + ret0, _ := ret[0].(*api.MetricsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPodMetrics indicates an expected call of GetPodMetrics. +func (mr *MockClientMockRecorder) GetPodMetrics(ctx, podName, q any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPodMetrics", reflect.TypeOf((*MockClient)(nil).GetPodMetrics), ctx, podName, q) +} + +// GetProject mocks base method. +func (m *MockClient) GetProject(ctx context.Context, projectID string) (*api.Project, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProject", ctx, projectID) + ret0, _ := ret[0].(*api.Project) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProject indicates an expected call of GetProject. +func (mr *MockClientMockRecorder) GetProject(ctx, projectID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProject", reflect.TypeOf((*MockClient)(nil).GetProject), ctx, projectID) +} + +// ListBuilds mocks base method. +func (m *MockClient) ListBuilds(ctx context.Context, projectID string) ([]api.Build, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListBuilds", ctx, projectID) + ret0, _ := ret[0].([]api.Build) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListBuilds indicates an expected call of ListBuilds. +func (mr *MockClientMockRecorder) ListBuilds(ctx, projectID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBuilds", reflect.TypeOf((*MockClient)(nil).ListBuilds), ctx, projectID) +} + +// ListDeploys mocks base method. +func (m *MockClient) ListDeploys(ctx context.Context, projectID, envID string) ([]api.Deploy, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListDeploys", ctx, projectID, envID) + ret0, _ := ret[0].([]api.Deploy) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListDeploys indicates an expected call of ListDeploys. +func (mr *MockClientMockRecorder) ListDeploys(ctx, projectID, envID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDeploys", reflect.TypeOf((*MockClient)(nil).ListDeploys), ctx, projectID, envID) +} + +// ListDomains mocks base method. +func (m *MockClient) ListDomains(ctx context.Context, orgID string) ([]api.CustomDomain, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListDomains", ctx, orgID) + ret0, _ := ret[0].([]api.CustomDomain) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListDomains indicates an expected call of ListDomains. +func (mr *MockClientMockRecorder) ListDomains(ctx, orgID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDomains", reflect.TypeOf((*MockClient)(nil).ListDomains), ctx, orgID) +} + +// ListEnvironments mocks base method. +func (m *MockClient) ListEnvironments(ctx context.Context, projectID string) ([]api.Environment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListEnvironments", ctx, projectID) + ret0, _ := ret[0].([]api.Environment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListEnvironments indicates an expected call of ListEnvironments. +func (mr *MockClientMockRecorder) ListEnvironments(ctx, projectID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListEnvironments", reflect.TypeOf((*MockClient)(nil).ListEnvironments), ctx, projectID) +} + +// ListLocations mocks base method. +func (m *MockClient) ListLocations(ctx context.Context) ([]api.Location, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListLocations", ctx) + ret0, _ := ret[0].([]api.Location) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListLocations indicates an expected call of ListLocations. +func (mr *MockClientMockRecorder) ListLocations(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLocations", reflect.TypeOf((*MockClient)(nil).ListLocations), ctx) +} + +// ListMetricTypes mocks base method. +func (m *MockClient) ListMetricTypes(ctx context.Context) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListMetricTypes", ctx) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListMetricTypes indicates an expected call of ListMetricTypes. +func (mr *MockClientMockRecorder) ListMetricTypes(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMetricTypes", reflect.TypeOf((*MockClient)(nil).ListMetricTypes), ctx) +} + +// ListOrganizations mocks base method. +func (m *MockClient) ListOrganizations(ctx context.Context) ([]api.Organization, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListOrganizations", ctx) + ret0, _ := ret[0].([]api.Organization) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListOrganizations indicates an expected call of ListOrganizations. +func (mr *MockClientMockRecorder) ListOrganizations(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListOrganizations", reflect.TypeOf((*MockClient)(nil).ListOrganizations), ctx) +} + +// ListPods mocks base method. +func (m *MockClient) ListPods(ctx context.Context, scope api.PodScope, limit int) ([]api.Pod, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListPods", ctx, scope, limit) + ret0, _ := ret[0].([]api.Pod) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListPods indicates an expected call of ListPods. +func (mr *MockClientMockRecorder) ListPods(ctx, scope, limit any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPods", reflect.TypeOf((*MockClient)(nil).ListPods), ctx, scope, limit) +} + +// ListProjects mocks base method. +func (m *MockClient) ListProjects(ctx context.Context, orgID string) ([]api.Project, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListProjects", ctx, orgID) + ret0, _ := ret[0].([]api.Project) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListProjects indicates an expected call of ListProjects. +func (mr *MockClientMockRecorder) ListProjects(ctx, orgID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListProjects", reflect.TypeOf((*MockClient)(nil).ListProjects), ctx, orgID) +} + +// ListRoutes mocks base method. +func (m *MockClient) ListRoutes(ctx context.Context, deployID string) ([]api.Route, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRoutes", ctx, deployID) + ret0, _ := ret[0].([]api.Route) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRoutes indicates an expected call of ListRoutes. +func (mr *MockClientMockRecorder) ListRoutes(ctx, deployID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRoutes", reflect.TypeOf((*MockClient)(nil).ListRoutes), ctx, deployID) +} + +// ListSecrets mocks base method. +func (m *MockClient) ListSecrets(ctx context.Context, projectID, envID string, limit int) ([]api.EnvironmentSecret, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListSecrets", ctx, projectID, envID, limit) + ret0, _ := ret[0].([]api.EnvironmentSecret) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListSecrets indicates an expected call of ListSecrets. +func (mr *MockClientMockRecorder) ListSecrets(ctx, projectID, envID, limit any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSecrets", reflect.TypeOf((*MockClient)(nil).ListSecrets), ctx, projectID, envID, limit) +} + +// SetSecret mocks base method. +func (m *MockClient) SetSecret(ctx context.Context, projectID, envID, name, value string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetSecret", ctx, projectID, envID, name, value) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetSecret indicates an expected call of SetSecret. +func (mr *MockClientMockRecorder) SetSecret(ctx, projectID, envID, name, value any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSecret", reflect.TypeOf((*MockClient)(nil).SetSecret), ctx, projectID, envID, name, value) +} + +// UpdateDeploy mocks base method. +func (m *MockClient) UpdateDeploy(ctx context.Context, deployID string, mutate func(*api.UpdateDeployRequest) error) (*api.Deploy, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateDeploy", ctx, deployID, mutate) + ret0, _ := ret[0].(*api.Deploy) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateDeploy indicates an expected call of UpdateDeploy. +func (mr *MockClientMockRecorder) UpdateDeploy(ctx, deployID, mutate any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeploy", reflect.TypeOf((*MockClient)(nil).UpdateDeploy), ctx, deployID, mutate) +} + +// VerifyDomain mocks base method. +func (m *MockClient) VerifyDomain(ctx context.Context, orgID, domain string) (*api.CustomDomain, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "VerifyDomain", ctx, orgID, domain) + ret0, _ := ret[0].(*api.CustomDomain) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// VerifyDomain indicates an expected call of VerifyDomain. +func (mr *MockClientMockRecorder) VerifyDomain(ctx, orgID, domain any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyDomain", reflect.TypeOf((*MockClient)(nil).VerifyDomain), ctx, orgID, domain) +} + +// MockTokenSource is a mock of TokenSource interface. +type MockTokenSource struct { + ctrl *gomock.Controller + recorder *MockTokenSourceMockRecorder + isgomock struct{} +} + +// MockTokenSourceMockRecorder is the mock recorder for MockTokenSource. +type MockTokenSourceMockRecorder struct { + mock *MockTokenSource +} + +// NewMockTokenSource creates a new mock instance. +func NewMockTokenSource(ctrl *gomock.Controller) *MockTokenSource { + mock := &MockTokenSource{ctrl: ctrl} + mock.recorder = &MockTokenSourceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTokenSource) EXPECT() *MockTokenSourceMockRecorder { + return m.recorder +} + +// Token mocks base method. +func (m *MockTokenSource) Token(ctx context.Context) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Token", ctx) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Token indicates an expected call of Token. +func (mr *MockTokenSourceMockRecorder) Token(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Token", reflect.TypeOf((*MockTokenSource)(nil).Token), ctx) +} diff --git a/internal/api/mock/mock_test.go b/internal/api/mock/mock_test.go new file mode 100644 index 0000000..822a49c --- /dev/null +++ b/internal/api/mock/mock_test.go @@ -0,0 +1,26 @@ +package mock_test + +import ( + "testing" + + "go.uber.org/mock/gomock" + + "github.com/devlyft/cli/internal/api" + "github.com/devlyft/cli/internal/api/mock" +) + +// The pattern command tests follow: depend on api.Client and expect calls +// on the generated mock. +func TestMockClient(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + c := mock.NewMockClient(ctrl) + c.EXPECT().ListOrganizations(gomock.Any()).Return([]api.Organization{{Name: "Acme"}}, nil) + + var client api.Client = c + orgs, err := client.ListOrganizations(t.Context()) + if err != nil || len(orgs) != 1 || orgs[0].Name != "Acme" { + t.Errorf("got %v, %v", orgs, err) + } +} diff --git a/internal/api/observe.go b/internal/api/observe.go new file mode 100644 index 0000000..f2a6b81 --- /dev/null +++ b/internal/api/observe.go @@ -0,0 +1,116 @@ +package api + +import ( + "context" + + "github.com/devlyft/cli/internal/api/gen/logs" + "github.com/devlyft/cli/internal/api/gen/metrics" +) + +// Logs are served by logs-api and metrics by metrics-api. Following logs +// over the WebSocket is hand-written in internal/api/logstream (#254). + +// LogsQuery narrows a log read. Empty fields use the server's defaults. +type LogsQuery struct { + // Pod narrows to one pod name. + Pod string + // Since is how far back to read, as a Go duration such as "1h". + Since string + // Filter is a case-insensitive substring each line must contain. + Filter string + // Match are structured field filters such as "status=running". + Match []string + // Limit is the maximum number of lines. + Limit int +} + +func (c *client) GetLogs(ctx context.Context, deployID string, q *LogsQuery) (*LogsResponse, error) { + id, err := parseID("deploy", deployID) + if err != nil { + return nil, err + } + params := &logs.GetV1DeploysDeployIdLogsParams{ + Pod: optString(q.Pod), + Since: optString(q.Since), + Filter: optString(q.Filter), + } + if len(q.Match) > 0 { + params.Match = &q.Match + } + if q.Limit > 0 { + params.Limit = int32Ptr(q.Limit) + } + ctx, n := withAttempts(ctx) + r, err := c.logs.GetV1DeploysDeployIdLogsWithResponse(ctx, id, params) + if err != nil { + return nil, c.transportError(err) + } + return expect(r.HTTPResponse, r.Body, r.JSON200, *n) +} + +// MetricsQuery selects a metric series. Metric is required; empty optional +// fields use the server's defaults. +type MetricsQuery struct { + Metric string + // Pod narrows a deploy's metric to one pod name. + Pod string + // Range is how far back to read, as a Go duration such as "1h". + Range string + // Step is the resolution between points, as a Go duration. + Step string +} + +func (c *client) ListMetricTypes(ctx context.Context) ([]string, error) { + ctx, n := withAttempts(ctx) + r, err := c.metrics.GetV1MetricTypesWithResponse(ctx) + if err != nil { + return nil, c.transportError(err) + } + v, err := expect(r.HTTPResponse, r.Body, r.JSON200, *n) + if err != nil { + return nil, err + } + return v.Metrics, nil +} + +func (c *client) GetDeployMetrics(ctx context.Context, deployID string, q MetricsQuery) (*MetricsResponse, error) { + id, err := parseID("deploy", deployID) + if err != nil { + return nil, err + } + params := &metrics.GetV1DeploysDeployIdMetricsParams{ + Metric: q.Metric, + Pod: optString(q.Pod), + Range: optString(q.Range), + Step: optString(q.Step), + } + ctx, n := withAttempts(ctx) + r, err := c.metrics.GetV1DeploysDeployIdMetricsWithResponse(ctx, id, params) + if err != nil { + return nil, c.transportError(err) + } + return expect(r.HTTPResponse, r.Body, r.JSON200, *n) +} + +// GetPodMetrics takes the pod's name, which is how metrics-api identifies +// pods. +func (c *client) GetPodMetrics(ctx context.Context, podName string, q MetricsQuery) (*MetricsResponse, error) { + params := &metrics.GetV1PodsPodIdMetricsParams{ + Metric: q.Metric, + Range: optString(q.Range), + Step: optString(q.Step), + } + ctx, n := withAttempts(ctx) + r, err := c.metrics.GetV1PodsPodIdMetricsWithResponse(ctx, podName, params) + if err != nil { + return nil, c.transportError(err) + } + return expect(r.HTTPResponse, r.Body, r.JSON200, *n) +} + +func optString(s string) *string { + if s == "" { + return nil + } + return &s +} diff --git a/internal/api/org.go b/internal/api/org.go new file mode 100644 index 0000000..c60fc5f --- /dev/null +++ b/internal/api/org.go @@ -0,0 +1,247 @@ +package api + +import ( + "context" + + "github.com/devlyft/cli/internal/api/gen/org" + "github.com/devlyft/cli/internal/clierr" +) + +// Organizations, projects, environments, secrets, pods and locations are +// served by org-api. Pods are read through org-api, which authorizes the +// caller; the CLI never talks to the pods service directly. + +func (c *client) ListOrganizations(ctx context.Context) ([]Organization, error) { + ctx, n := withAttempts(ctx) + r, err := c.org.GetV1OrganizationsWithResponse(ctx) + if err != nil { + return nil, c.transportError(err) + } + v, err := expect(r.HTTPResponse, r.Body, r.JSON200, *n) + if err != nil { + return nil, err + } + return v.Organizations, nil +} + +func (c *client) GetOrganization(ctx context.Context, orgID string) (*Organization, error) { + id, err := parseID("organization", orgID) + if err != nil { + return nil, err + } + ctx, n := withAttempts(ctx) + r, err := c.org.GetV1OrganizationsOrganizationIdWithResponse(ctx, id) + if err != nil { + return nil, c.transportError(err) + } + return expect(r.HTTPResponse, r.Body, r.JSON200, *n) +} + +func (c *client) ListProjects(ctx context.Context, orgID string) ([]Project, error) { + id, err := parseID("organization", orgID) + if err != nil { + return nil, err + } + ctx, n := withAttempts(ctx) + r, err := c.org.GetV1OrganizationsOrganizationIdProjectsWithResponse(ctx, id) + if err != nil { + return nil, c.transportError(err) + } + v, err := expect(r.HTTPResponse, r.Body, r.JSON200, *n) + if err != nil { + return nil, err + } + return v.Projects, nil +} + +func (c *client) GetProject(ctx context.Context, projectID string) (*Project, error) { + id, err := parseID("project", projectID) + if err != nil { + return nil, err + } + ctx, n := withAttempts(ctx) + r, err := c.org.GetV1ProjectsProjectIdWithResponse(ctx, id) + if err != nil { + return nil, c.transportError(err) + } + return expect(r.HTTPResponse, r.Body, r.JSON200, *n) +} + +func (c *client) ListEnvironments(ctx context.Context, projectID string) ([]Environment, error) { + id, err := parseID("project", projectID) + if err != nil { + return nil, err + } + ctx, n := withAttempts(ctx) + r, err := c.org.GetV1ProjectsProjectIdEnvironmentsWithResponse(ctx, id) + if err != nil { + return nil, c.transportError(err) + } + v, err := expect(r.HTTPResponse, r.Body, r.JSON200, *n) + if err != nil { + return nil, err + } + return v.Environments, nil +} + +func (c *client) GetEnvironment(ctx context.Context, envID string) (*Environment, error) { + id, err := parseID("environment", envID) + if err != nil { + return nil, err + } + ctx, n := withAttempts(ctx) + r, err := c.org.GetV1EnvironmentsEnvironmentIdWithResponse(ctx, id) + if err != nil { + return nil, c.transportError(err) + } + return expect(r.HTTPResponse, r.Body, r.JSON200, *n) +} + +func (c *client) ListLocations(ctx context.Context) ([]Location, error) { + ctx, n := withAttempts(ctx) + r, err := c.org.GetV1LocationsWithResponse(ctx) + if err != nil { + return nil, c.transportError(err) + } + v, err := expect(r.HTTPResponse, r.Body, r.JSON200, *n) + if err != nil { + return nil, err + } + return v.Locations, nil +} + +func (c *client) ListSecrets(ctx context.Context, projectID, envID string, limit int) ([]EnvironmentSecret, error) { + pid, err := parseID("project", projectID) + if err != nil { + return nil, err + } + eid, err := parseID("environment", envID) + if err != nil { + return nil, err + } + return Paged(ctx, limit, func(ctx context.Context, p Page) ([]EnvironmentSecret, int, error) { + ctx, n := withAttempts(ctx) + params := &org.ListEnvironmentSecretsV1Params{Limit: int32Ptr(p.Limit), Offset: int32Ptr(p.Offset)} + r, err := c.org.ListEnvironmentSecretsV1WithResponse(ctx, pid, eid, params) + if err != nil { + return nil, 0, c.transportError(err) + } + v, err := expect(r.HTTPResponse, r.Body, r.JSON200, *n) + if err != nil { + return nil, 0, err + } + return v.Secrets, int(v.Total), nil + }) +} + +func (c *client) SetSecret(ctx context.Context, projectID, envID, name, value string) error { + pid, err := parseID("project", projectID) + if err != nil { + return err + } + eid, err := parseID("environment", envID) + if err != nil { + return err + } + ctx, n := withAttempts(ctx) + r, err := c.org.CreateEnvironmentSecretV1WithResponse(ctx, pid, eid, name, org.CreateEnvironmentSecretV1JSONRequestBody{Value: value}) + if err != nil { + return c.transportError(err) + } + return checkStatus(r.HTTPResponse, r.Body, *n) +} + +func (c *client) DeleteSecret(ctx context.Context, projectID, envID, name string) error { + pid, err := parseID("project", projectID) + if err != nil { + return err + } + eid, err := parseID("environment", envID) + if err != nil { + return err + } + ctx, n := withAttempts(ctx) + r, err := c.org.DeleteEnvironmentSecretV1WithResponse(ctx, pid, eid, name) + if err != nil { + return c.transportError(err) + } + return checkStatus(r.HTTPResponse, r.Body, *n) +} + +// PodScope selects the pods to list: set exactly one ID. The most specific +// one wins if several are set. +type PodScope struct { + ProjectID string + EnvironmentID string + DeployID string +} + +func (c *client) ListPods(ctx context.Context, scope PodScope, limit int) ([]Pod, error) { + var fetch PageFunc[Pod] + switch { + case scope.DeployID != "": + id, err := parseID("deploy", scope.DeployID) + if err != nil { + return nil, err + } + fetch = func(ctx context.Context, p Page) ([]Pod, int, error) { + ctx, n := withAttempts(ctx) + r, err := c.org.GetV1DeploysDeployIdPodsWithResponse(ctx, id, &org.GetV1DeploysDeployIdPodsParams{Limit: int32Ptr(p.Limit), Offset: int32Ptr(p.Offset)}) + if err != nil { + return nil, 0, c.transportError(err) + } + return podsPage(r.HTTPResponse, r.Body, r.JSON200, *n) + } + case scope.EnvironmentID != "": + id, err := parseID("environment", scope.EnvironmentID) + if err != nil { + return nil, err + } + fetch = func(ctx context.Context, p Page) ([]Pod, int, error) { + ctx, n := withAttempts(ctx) + r, err := c.org.GetV1EnvironmentsEnvironmentIdPodsWithResponse(ctx, id, &org.GetV1EnvironmentsEnvironmentIdPodsParams{Limit: int32Ptr(p.Limit), Offset: int32Ptr(p.Offset)}) + if err != nil { + return nil, 0, c.transportError(err) + } + return podsPage(r.HTTPResponse, r.Body, r.JSON200, *n) + } + case scope.ProjectID != "": + id, err := parseID("project", scope.ProjectID) + if err != nil { + return nil, err + } + fetch = func(ctx context.Context, p Page) ([]Pod, int, error) { + ctx, n := withAttempts(ctx) + r, err := c.org.GetV1ProjectsProjectIdPodsWithResponse(ctx, id, &org.GetV1ProjectsProjectIdPodsParams{Limit: int32Ptr(p.Limit), Offset: int32Ptr(p.Offset)}) + if err != nil { + return nil, 0, c.transportError(err) + } + return podsPage(r.HTTPResponse, r.Body, r.JSON200, *n) + } + default: + return nil, clierr.Usagef("%v", errNoScope) + } + return Paged(ctx, limit, fetch) +} + +func podsPage(resp *httpResponse, body []byte, v *org.ListPodsResponse, attempts int) ([]Pod, int, error) { + v, err := expect(resp, body, v, attempts) + if err != nil { + return nil, 0, err + } + return v.Results, int(v.Total), nil +} + +func (c *client) GetPod(ctx context.Context, podID string) (*Pod, error) { + ctx, n := withAttempts(ctx) + r, err := c.org.GetV1PodsPodIdWithResponse(ctx, podID) + if err != nil { + return nil, c.transportError(err) + } + return expect(r.HTTPResponse, r.Body, r.JSON200, *n) +} + +func int32Ptr(v int) *int32 { + i := int32(min(v, MaxPageSize*1000)) //nolint:gosec // bounded above + return &i +} diff --git a/internal/api/paging.go b/internal/api/paging.go new file mode 100644 index 0000000..9524aa7 --- /dev/null +++ b/internal/api/paging.go @@ -0,0 +1,49 @@ +package api + +import "context" + +// MaxPageSize is the largest page the paged list endpoints accept. +const MaxPageSize = 100 + +// Page selects a page of a paged list endpoint. +type Page struct { + Limit int + Offset int +} + +// PageFunc fetches one page and reports the total number of items the +// endpoint has. +type PageFunc[T any] func(ctx context.Context, p Page) (items []T, total int, err error) + +// maxPages stops a server that ignores offset from looping forever. +const maxPages = 10000 + +// Paged collects up to limit items (all items when limit <= 0) from an +// endpoint that supports limit and offset, fetching as few pages as needed. +func Paged[T any](ctx context.Context, limit int, fetch PageFunc[T]) ([]T, error) { + var out []T + for range maxPages { + size := MaxPageSize + if limit > 0 { + size = min(size, limit-len(out)) + } + items, total, err := fetch(ctx, Page{Limit: size, Offset: len(out)}) + if err != nil { + return nil, err + } + out = append(out, items...) + if len(items) == 0 || len(items) < size || len(out) >= total || (limit > 0 && len(out) >= limit) { + break + } + } + return Trim(out, limit), nil +} + +// Trim returns at most limit items (all items when limit <= 0). Use it for +// endpoints without server-side paging, which return everything at once. +func Trim[T any](items []T, limit int) []T { + if limit > 0 && len(items) > limit { + return items[:limit] + } + return items +} diff --git a/internal/api/paging_test.go b/internal/api/paging_test.go new file mode 100644 index 0000000..a71107a --- /dev/null +++ b/internal/api/paging_test.go @@ -0,0 +1,104 @@ +package api + +import ( + "context" + "errors" + "slices" + "testing" +) + +// pagedServer serves n items honouring limit and offset. +func pagedServer(n int, calls *[]Page) PageFunc[int] { + return func(_ context.Context, p Page) ([]int, int, error) { + *calls = append(*calls, p) + var items []int + for i := p.Offset; i < n && len(items) < p.Limit; i++ { + items = append(items, i) + } + return items, n, nil + } +} + +func TestPaged(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + total int + limit int + wantLen int + wantCalls []Page + }{ + {"limit within one page", 500, 50, 50, []Page{{50, 0}}}, + {"limit across pages", 500, 150, 150, []Page{{100, 0}, {50, 100}}}, + {"all items", 230, 0, 230, []Page{{100, 0}, {100, 100}, {100, 200}}}, + {"limit above total", 30, 50, 30, []Page{{50, 0}}}, + {"empty", 0, 0, 0, []Page{{100, 0}}}, + {"exact page boundary", 200, 0, 200, []Page{{100, 0}, {100, 100}}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var calls []Page + got, err := Paged(t.Context(), tt.limit, pagedServer(tt.total, &calls)) + if err != nil { + t.Fatal(err) + } + if len(got) != tt.wantLen { + t.Errorf("len = %d, want %d", len(got), tt.wantLen) + } + for i, v := range got { + if v != i { + t.Fatalf("item %d = %d: items out of order or duplicated", i, v) + } + } + if !slices.Equal(calls, tt.wantCalls) { + t.Errorf("calls = %v, want %v", calls, tt.wantCalls) + } + }) + } +} + +// A server that ignores limit and offset returns everything every time; +// Paged must stop and trim instead of looping. +func TestPagedServerIgnoresPaging(t *testing.T) { + t.Parallel() + + calls := 0 + all := []int{0, 1, 2, 3, 4, 5, 6} + got, err := Paged(t.Context(), 5, func(context.Context, Page) ([]int, int, error) { + calls++ + return all, len(all), nil + }) + if err != nil { + t.Fatal(err) + } + if !slices.Equal(got, all[:5]) || calls != 1 { + t.Errorf("got %v after %d calls", got, calls) + } +} + +func TestPagedError(t *testing.T) { + t.Parallel() + + boom := errors.New("boom") + _, err := Paged(t.Context(), 0, func(context.Context, Page) ([]int, int, error) { return nil, 0, boom }) + if !errors.Is(err, boom) { + t.Errorf("err = %v", err) + } +} + +func TestTrim(t *testing.T) { + t.Parallel() + + items := []string{"a", "b", "c"} + if got := Trim(items, 2); !slices.Equal(got, []string{"a", "b"}) { + t.Errorf("Trim(2) = %v", got) + } + if got := Trim(items, 0); !slices.Equal(got, items) { + t.Errorf("Trim(0) = %v", got) + } + if got := Trim(items, 10); !slices.Equal(got, items) { + t.Errorf("Trim(10) = %v", got) + } +} diff --git a/internal/api/transport.go b/internal/api/transport.go new file mode 100644 index 0000000..c7d956c --- /dev/null +++ b/internal/api/transport.go @@ -0,0 +1,284 @@ +package api + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "log/slog" + "math/rand/v2" + "net/http" + "strconv" + "time" + + "github.com/devlyft/cli/internal/redact" +) + +// RetryPolicy controls how idempotent requests are retried. +type RetryPolicy struct { + // MaxAttempts is the total number of attempts, including the first. + MaxAttempts int + // BaseDelay is the backoff before the second attempt; it doubles for + // each further attempt, with full jitter. + BaseDelay time.Duration + // MaxDelay caps a single backoff, and a Retry-After the server asks for. + // A longer Retry-After ends the retries instead. + MaxDelay time.Duration + // MaxTotalWait caps the time spent waiting across all retries. + MaxTotalWait time.Duration +} + +// DefaultRetryPolicy is used when Options.Retry is the zero value. +var DefaultRetryPolicy = RetryPolicy{ + MaxAttempts: 4, + BaseDelay: 500 * time.Millisecond, + MaxDelay: 10 * time.Second, + MaxTotalWait: 30 * time.Second, +} + +// maxLoggedBody bounds request and response bodies in debug logs. +const maxLoggedBody = 4096 + +// doer sends requests for the generated clients: it adds the user agent +// and bearer token, retries idempotent requests and logs each exchange. +type doer struct { + hc *http.Client + tokens TokenSource + ua string + log *slog.Logger + policy RetryPolicy + + // Replaced in tests. + sleep func(ctx context.Context, d time.Duration) error + now func() time.Time + jitter func(n int64) int64 +} + +func newDoer(opts *Options) *doer { + policy := opts.Retry + if policy == (RetryPolicy{}) { + policy = DefaultRetryPolicy + } + return &doer{ + hc: opts.HTTPClient, + tokens: opts.Tokens, + ua: opts.UserAgent, + log: opts.Logger, + policy: policy, + sleep: sleepCtx, + now: time.Now, + jitter: rand.Int64N, //nolint:gosec // jitter needs no cryptographic randomness + } +} + +// idempotent methods are safe to retry. POST never is: `deploy` is safe to +// re-run because it upserts by name, not because requests are retried. +func idempotent(method string) bool { + switch method { + case http.MethodGet, http.MethodHead, http.MethodPut, http.MethodDelete, http.MethodOptions: + return true + } + return false +} + +// retryableStatus reports whether a response status is worth retrying. +func retryableStatus(code int) bool { + return code == http.StatusTooManyRequests || + (code >= http.StatusInternalServerError && code != http.StatusNotImplemented) +} + +// Do implements the generated clients' HttpRequestDoer. +func (d *doer) Do(req *http.Request) (*http.Response, error) { + ctx := req.Context() + attempts := attemptsFrom(ctx) + var waited time.Duration + + for attempt := 1; ; attempt++ { + *attempts = attempt + r, err := d.prepare(req, attempt) + if err != nil { + return nil, err + } + + start := d.now() + resp, err := d.hc.Do(r) //nolint:gosec // the URL is the configured API origin + d.logExchange(r, resp, err, attempt, d.now().Sub(start)) + + if ctxErr := ctx.Err(); ctxErr != nil { + closeBody(resp) + return nil, ctxErr + } + retry := idempotent(req.Method) && attempt < d.policy.MaxAttempts && + (err != nil || retryableStatus(resp.StatusCode)) + if !retry { + if err != nil && attempt > 1 { + return nil, fmt.Errorf("%w (after %d attempts)", err, attempt) + } + return resp, err + } + + wait, ok := d.backoff(attempt, resp) + if !ok || waited+wait > d.policy.MaxTotalWait { + // Out of patience: report what we have. + if err != nil { + return nil, fmt.Errorf("%w (after %d attempts)", err, attempt) + } + return resp, nil + } + closeBody(resp) + if d.log.Enabled(ctx, slog.LevelDebug) { + d.log.Debug("retrying", "method", req.Method, "url", redact.String(req.URL.String()), "attempt", attempt+1, "wait", wait) + } + if err := d.sleep(ctx, wait); err != nil { + return nil, err + } + waited += wait + } +} + +// prepare clones req for one attempt with a fresh body and current token. +func (d *doer) prepare(req *http.Request, attempt int) (*http.Request, error) { + r := req.Clone(req.Context()) + if attempt > 1 && req.Body != nil && req.Body != http.NoBody { + if req.GetBody == nil { + return nil, errors.New("request body cannot be replayed for a retry") + } + body, err := req.GetBody() + if err != nil { + return nil, err + } + r.Body = body + } + if d.ua != "" { + r.Header.Set("User-Agent", d.ua) + } + r.Header.Set("Accept", "application/json") + if d.tokens != nil { + // Asked for on every attempt so a refreshed token is picked up. + token, err := d.tokens.Token(req.Context()) + if err != nil { + return nil, err + } + if token != "" { + r.Header.Set("Authorization", "Bearer "+token) + } + } + return r, nil +} + +// backoff returns how long to wait before the next attempt, and false when +// the server asked for a longer wait than MaxDelay. +func (d *doer) backoff(attempt int, resp *http.Response) (time.Duration, bool) { + if resp != nil { + if ra, ok := parseRetryAfter(resp.Header.Get("Retry-After"), d.now()); ok { + return ra, ra <= d.policy.MaxDelay + } + } + ceiling := min(d.policy.BaseDelay<<(attempt-1), d.policy.MaxDelay) + if ceiling <= 0 { + return 0, true + } + return time.Duration(d.jitter(int64(ceiling) + 1)), true +} + +// parseRetryAfter reads a Retry-After header in seconds or as an HTTP date. +func parseRetryAfter(v string, now time.Time) (time.Duration, bool) { + if v == "" { + return 0, false + } + if secs, err := strconv.Atoi(v); err == nil { + if secs < 0 { + return 0, false + } + return time.Duration(secs) * time.Second, true + } + if t, err := http.ParseTime(v); err == nil { + return max(t.Sub(now), 0), true + } + return 0, false +} + +func sleepCtx(ctx context.Context, d time.Duration) error { + t := time.NewTimer(d) + defer t.Stop() + select { + case <-ctx.Done(): + return ctx.Err() + case <-t.C: + return nil + } +} + +func closeBody(resp *http.Response) { + if resp != nil && resp.Body != nil { + _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1<<16)) + _ = resp.Body.Close() + } +} + +// logExchange writes one request and response to the debug log, redacted. +// Bodies are read only when debug logging is on. +func (d *doer) logExchange(req *http.Request, resp *http.Response, err error, attempt int, took time.Duration) { + ctx := req.Context() + if !d.log.Enabled(ctx, slog.LevelDebug) { + return + } + attrs := []any{ + "method", req.Method, + "url", redact.String(req.URL.String()), + "attempt", attempt, + "duration", took.Round(time.Millisecond), + "request_headers", redact.Header(req.Header), + } + if req.GetBody != nil { + if body, berr := req.GetBody(); berr == nil { + attrs = append(attrs, "request_body", readLogged(body)) + } + } + if err != nil { + d.log.Debug("http error", append(attrs, "error", redact.String(err.Error()))...) + return + } + attrs = append(attrs, "status", resp.StatusCode, "response_headers", redact.Header(resp.Header)) + if resp.Body != nil { + // Read the body for the log and put it back for the caller. + b, rerr := io.ReadAll(resp.Body) + _ = resp.Body.Close() + resp.Body = io.NopCloser(bytes.NewReader(b)) + if rerr == nil { + attrs = append(attrs, "response_body", truncate(b)) + } + } + d.log.Debug("http", attrs...) +} + +func readLogged(r io.ReadCloser) string { + defer func() { _ = r.Close() }() + b, _ := io.ReadAll(io.LimitReader(r, maxLoggedBody+1)) + return truncate(b) +} + +func truncate(b []byte) string { + s := string(b) + if len(s) > maxLoggedBody { + s = s[:maxLoggedBody] + "…(truncated)" + } + return redact.String(s) +} + +// attemptsKey carries a counter the doer updates, so errors can say how +// many attempts were made. +type attemptsKey struct{} + +func withAttempts(ctx context.Context) (context.Context, *int) { + n := new(int) + return context.WithValue(ctx, attemptsKey{}, n), n +} + +func attemptsFrom(ctx context.Context) *int { + if n, ok := ctx.Value(attemptsKey{}).(*int); ok { + return n + } + return new(int) +} diff --git a/internal/api/transport_test.go b/internal/api/transport_test.go new file mode 100644 index 0000000..53f6229 --- /dev/null +++ b/internal/api/transport_test.go @@ -0,0 +1,446 @@ +package api + +import ( + "bytes" + "context" + "errors" + "io" + "log/slog" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/devlyft/cli/internal/clierr" + "github.com/devlyft/cli/internal/cmdutil" + "github.com/devlyft/cli/internal/exitcode" +) + +const ( + testDeployID = "6f1c6a3e-0d6b-4f6e-9a55-3f7f1d2b9c01" + testProject = "0b4e1a3c-6c2f-4d0a-8c51-5f0f1e2d3c4b" + testEnv = "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d" +) + +// fakeClock records waits instead of sleeping. +type fakeClock struct { + mu sync.Mutex + now time.Time + waits []time.Duration +} + +func (f *fakeClock) sleep(ctx context.Context, d time.Duration) error { + f.mu.Lock() + defer f.mu.Unlock() + f.waits = append(f.waits, d) + f.now = f.now.Add(d) + return ctx.Err() +} + +func (f *fakeClock) Now() time.Time { + f.mu.Lock() + defer f.mu.Unlock() + return f.now +} + +// newTestClient returns a client for srv that waits on a fake clock and +// always picks the longest jittered backoff. +func newTestClient(t *testing.T, srv *httptest.Server, log *slog.Logger) (*client, *fakeClock) { + t.Helper() + cl, err := New(Options{BaseURL: srv.URL, Tokens: StaticToken("s3cret-token"), HTTPClient: srv.Client(), Logger: log, UserAgent: "devlyft/test"}) + if err != nil { + t.Fatal(err) + } + c := cl.(*client) //nolint:forcetypeassert // New always returns *client + clock := &fakeClock{now: time.Date(2026, 10, 25, 12, 0, 0, 0, time.UTC)} + c.doer.sleep = clock.sleep + c.doer.now = clock.Now + c.doer.jitter = func(n int64) int64 { return n - 1 } + return c, clock +} + +// scripted serves the given statuses in order, then 200 with body. +type scripted struct { + mu sync.Mutex + statuses []int + headers []http.Header + body string + requests []*http.Request + bodies []string +} + +func (s *scripted) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.mu.Lock() + defer s.mu.Unlock() + b, _ := io.ReadAll(r.Body) + s.requests = append(s.requests, r) + s.bodies = append(s.bodies, string(b)) + i := len(s.requests) - 1 + status := http.StatusOK + if i < len(s.statuses) { + status = s.statuses[i] + } + if i < len(s.headers) { + for k, vs := range s.headers[i] { + w.Header()[k] = vs + } + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if status < 300 { + _, _ = io.WriteString(w, s.body) + } else { + _, _ = io.WriteString(w, `{"error":"try later"}`) + } +} + +const deployJSON = `{"id":"` + testDeployID + `","environment_id":"` + testEnv + `","build_id":"11111111-2222-4333-8444-555555555555",` + + `"name":"web","location_id":"eu-west","env":[{"kind":"env","key":"A","value":"1"}],"tenancy_type":"INHERIT","replicas":2,` + + `"expose":true,"enable_hpa":false,"hpa_min_replicas":null,"hpa_max_replicas":null,"hpa_target_cpu":null,` + + `"placement":{"state":"PLACED"},"has_registry_credential":false,"registry_credential_name":null,` + + `"created_at":"2026-10-01T00:00:00Z","updated_at":"2026-10-01T00:00:00Z"}` + +func TestRetryPolicy(t *testing.T) { + t.Parallel() + + retryAt := time.Date(2026, 10, 25, 12, 0, 7, 0, time.UTC).Format(http.TimeFormat) + tests := []struct { + name string + statuses []int + headers []http.Header + call func(context.Context, *client) error + requests int + waits []time.Duration + wantExit int + wantInMsg string + }{ + { + name: "503 then 200", + statuses: []int{503}, + call: getDeploy, + requests: 2, + waits: []time.Duration{500 * time.Millisecond}, + wantExit: exitcode.OK, + }, + { + name: "429 honours Retry-After seconds", + statuses: []int{429}, + headers: []http.Header{{"Retry-After": {"3"}}}, + call: getDeploy, + requests: 2, + waits: []time.Duration{3 * time.Second}, + wantExit: exitcode.OK, + }, + { + name: "429 honours Retry-After date", + statuses: []int{429}, + headers: []http.Header{{"Retry-After": {retryAt}}}, + call: getDeploy, + requests: 2, + waits: []time.Duration{7 * time.Second}, + wantExit: exitcode.OK, + }, + { + name: "Retry-After beyond MaxDelay is not waited for", + statuses: []int{429}, + headers: []http.Header{{"Retry-After": {"3600"}}}, + call: getDeploy, + requests: 1, + wantExit: exitcode.Error, + wantInMsg: "try later", + }, + { + name: "gives up after MaxAttempts with the attempt count", + statuses: []int{500, 502, 503, 504}, + call: getDeploy, + requests: 4, + waits: []time.Duration{500 * time.Millisecond, time.Second, 2 * time.Second}, + wantExit: exitcode.Error, + wantInMsg: "(after 4 attempts)", + }, + { + name: "PUT is retried", + statuses: []int{200, 503}, + call: func(ctx context.Context, c *client) error { + _, err := c.UpdateDeploy(ctx, testDeployID, func(r *UpdateDeployRequest) error { r.Replicas = 3; return nil }) + return err + }, + requests: 3, + waits: []time.Duration{500 * time.Millisecond}, + wantExit: exitcode.OK, + }, + { + name: "DELETE is retried", + statuses: []int{502, 204}, + call: func(ctx context.Context, c *client) error { return c.DeleteDeploy(ctx, testDeployID) }, + requests: 2, + waits: []time.Duration{500 * time.Millisecond}, + wantExit: exitcode.OK, + }, + { + name: "POST is never retried", + statuses: []int{503}, + call: func(ctx context.Context, c *client) error { + _, err := c.CreateRoute(ctx, testDeployID, CreateRouteRequest{Hostname: "www.example.com"}) + return err + }, + requests: 1, + wantExit: exitcode.Error, + wantInMsg: "try later", + }, + { + name: "4xx is not retried", + statuses: []int{404}, + call: getDeploy, + requests: 1, + wantExit: exitcode.NotFound, + }, + { + name: "501 is not retried", + statuses: []int{501}, + call: getDeploy, + requests: 1, + wantExit: exitcode.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + s := &scripted{statuses: tt.statuses, headers: tt.headers, body: deployJSON} + srv := httptest.NewServer(s) + defer srv.Close() + c, clock := newTestClient(t, srv, nil) + + err := tt.call(t.Context(), c) + + if got := clierr.ExitCode(err); got != tt.wantExit { + t.Fatalf("exit = %d, want %d (err %v)", got, tt.wantExit, err) + } + if tt.wantInMsg != "" && !strings.Contains(err.Error(), tt.wantInMsg) { + t.Errorf("error %q does not contain %q", err, tt.wantInMsg) + } + if len(s.requests) != tt.requests { + t.Errorf("requests = %d, want %d", len(s.requests), tt.requests) + } + if !equalDurations(clock.waits, tt.waits) { + t.Errorf("waits = %v, want %v", clock.waits, tt.waits) + } + }) + } +} + +func getDeploy(ctx context.Context, c *client) error { + _, err := c.GetDeploy(ctx, testDeployID) + return err +} + +func equalDurations(a, b []time.Duration) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func TestRetryReplaysBody(t *testing.T) { + t.Parallel() + + s := &scripted{statuses: []int{200, 503}, body: deployJSON} + srv := httptest.NewServer(s) + defer srv.Close() + c, _ := newTestClient(t, srv, nil) + + if _, err := c.UpdateDeploy(t.Context(), testDeployID, func(r *UpdateDeployRequest) error { r.Replicas = 5; return nil }); err != nil { + t.Fatal(err) + } + if s.bodies[1] == "" || s.bodies[1] != s.bodies[2] { + t.Errorf("retried PUT body differs:\n%s\n%s", s.bodies[1], s.bodies[2]) + } +} + +func TestRetryTotalWaitCap(t *testing.T) { + t.Parallel() + + s := &scripted{statuses: []int{429, 429, 429}, headers: []http.Header{{"Retry-After": {"10"}}, {"Retry-After": {"10"}}, {"Retry-After": {"10"}}}, body: deployJSON} + srv := httptest.NewServer(s) + defer srv.Close() + c, clock := newTestClient(t, srv, nil) + c.doer.policy.MaxTotalWait = 15 * time.Second + + err := getDeploy(t.Context(), c) + if err == nil || !strings.Contains(err.Error(), "(after 2 attempts)") { + t.Fatalf("err = %v, want failure after 2 attempts", err) + } + if len(clock.waits) != 1 { + t.Errorf("waits = %v, want one", clock.waits) + } +} + +func TestRetryStopsOnCancel(t *testing.T) { + t.Parallel() + + s := &scripted{statuses: []int{503, 503, 503}, body: deployJSON} + srv := httptest.NewServer(s) + defer srv.Close() + c, _ := newTestClient(t, srv, nil) + ctx, cancel := context.WithCancel(t.Context()) + c.doer.sleep = func(context.Context, time.Duration) error { + cancel() + return context.Canceled + } + + err := getDeploy(ctx, c) + if !errors.Is(err, context.Canceled) { + t.Fatalf("err = %v, want context.Canceled", err) + } + if clierr.ExitCode(err) != exitcode.Interrupted { + t.Errorf("exit = %d, want %d", clierr.ExitCode(err), exitcode.Interrupted) + } + if len(s.requests) != 1 { + t.Errorf("requests = %d, want 1", len(s.requests)) + } +} + +func TestConnectionErrorsRetriedForIdempotentOnly(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(http.NotFoundHandler()) + c, clock := newTestClient(t, srv, nil) + srv.Close() // every request now fails to connect + + err := getDeploy(t.Context(), c) + if clierr.ExitCode(err) != exitcode.Error || !strings.Contains(err.Error(), "after 4 attempts") { + t.Errorf("GET: err = %v", err) + } + if e := clierr.As(err); e.Code != clierr.CodeUnavailable || !e.Retryable { + t.Errorf("GET: code = %s retryable = %v", e.Code, e.Retryable) + } + if len(clock.waits) != 3 { + t.Errorf("GET: waits = %v, want 3", clock.waits) + } + + clock.waits = nil + _, err = c.CreateRoute(t.Context(), testDeployID, CreateRouteRequest{Hostname: "a.example.com"}) + if err == nil || len(clock.waits) != 0 { + t.Errorf("POST: err = %v, waits = %v; want an error and no retry", err, clock.waits) + } +} + +func TestParseRetryAfter(t *testing.T) { + t.Parallel() + + now := time.Date(2026, 10, 25, 12, 0, 0, 0, time.UTC) + tests := []struct { + in string + want time.Duration + wantOK bool + }{ + {"", 0, false}, + {"0", 0, true}, + {"5", 5 * time.Second, true}, + {"-1", 0, false}, + {"soon", 0, false}, + {now.Add(30 * time.Second).Format(http.TimeFormat), 30 * time.Second, true}, + {now.Add(-time.Minute).Format(http.TimeFormat), 0, true}, + } + for _, tt := range tests { + got, ok := parseRetryAfter(tt.in, now) + if got != tt.want || ok != tt.wantOK { + t.Errorf("parseRetryAfter(%q) = %v, %v; want %v, %v", tt.in, got, ok, tt.want, tt.wantOK) + } + } +} + +func TestRequestHeaders(t *testing.T) { + t.Parallel() + + s := &scripted{body: deployJSON} + srv := httptest.NewServer(s) + defer srv.Close() + c, _ := newTestClient(t, srv, nil) + + if err := getDeploy(t.Context(), c); err != nil { + t.Fatal(err) + } + h := s.requests[0].Header + if got := h.Get("Authorization"); got != "Bearer s3cret-token" { + t.Errorf("Authorization = %q", got) + } + if got := h.Get("User-Agent"); got != "devlyft/test" { + t.Errorf("User-Agent = %q", got) + } +} + +func TestTokenAskedOnEveryAttempt(t *testing.T) { + t.Parallel() + + s := &scripted{statuses: []int{503}, body: deployJSON} + srv := httptest.NewServer(s) + defer srv.Close() + c, _ := newTestClient(t, srv, nil) + n := 0 + c.doer.tokens = tokenFunc(func(context.Context) (string, error) { + n++ + return "token-" + strconv.Itoa(n), nil + }) + + if err := getDeploy(t.Context(), c); err != nil { + t.Fatal(err) + } + if a, b := s.requests[0].Header.Get("Authorization"), s.requests[1].Header.Get("Authorization"); a == b { + t.Errorf("both attempts used %q; want a fresh token per attempt", a) + } +} + +type tokenFunc func(context.Context) (string, error) + +func (f tokenFunc) Token(ctx context.Context) (string, error) { return f(ctx) } + +func TestDebugLogRedactsSecrets(t *testing.T) { + t.Parallel() + + s := &scripted{statuses: []int{201}, body: `{"key":"DB_URL"}`} + srv := httptest.NewServer(s) + defer srv.Close() + var buf bytes.Buffer + c, _ := newTestClient(t, srv, cmdutil.NewLogger(&buf, slog.LevelDebug, true)) + + if err := c.SetSecret(t.Context(), testProject, testEnv, "DB_URL", "postgres://u:hunter2@db/x"); err != nil { + t.Fatal(err) + } + out := buf.String() + for _, secret := range []string{"s3cret-token", "hunter2"} { + if strings.Contains(out, secret) { + t.Errorf("debug log leaked %q:\n%s", secret, out) + } + } + for _, want := range []string{"method=POST", "status=201", "DB_URL"} { + if !strings.Contains(out, want) { + t.Errorf("debug log missing %q:\n%s", want, out) + } + } +} + +func TestNoBodiesReadWhenNotDebugging(t *testing.T) { + t.Parallel() + + s := &scripted{body: deployJSON} + srv := httptest.NewServer(s) + defer srv.Close() + var buf bytes.Buffer + c, _ := newTestClient(t, srv, cmdutil.NewLogger(&buf, slog.LevelWarn, true)) + + if err := getDeploy(t.Context(), c); err != nil { + t.Fatal(err) + } + if buf.Len() != 0 { + t.Errorf("logged at warning level: %s", buf.String()) + } +} From 5478dce23fc11319ae2770bf9b75a3984eb8e23e Mon Sep 17 00:00:00 2001 From: RogerCecilpot Date: Sun, 25 Oct 2026 12:44:33 +0000 Subject: [PATCH 3/3] ci: keep code generation dependencies in step with the platform oapi-codegen v2.5.1 does not build against newer kin-openapi or oasdiff/yaml, so Dependabot must not bump them on their own. Refs devlyft/devlyft#236 Co-Authored-By: Claude Opus 5.5 --- .github/dependabot.yml | 6 ++++++ go.mod | 2 ++ 2 files changed, 8 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5d1b8dc..fad3e6b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,6 +9,12 @@ updates: groups: gomod: patterns: ["*"] + ignore: + # Code generation must match the platform: these move together with + # its oapi-codegen version, not on their own. + - dependency-name: github.com/oapi-codegen/* + - dependency-name: github.com/getkin/kin-openapi + - dependency-name: github.com/oasdiff/* - package-ecosystem: github-actions directory: / schedule: diff --git a/go.mod b/go.mod index 5eb697f..61699bd 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/devlyft/cli go 1.26.0 +toolchain go1.26.10 + require ( github.com/getkin/kin-openapi v0.133.0 github.com/google/uuid v1.6.0