mirror of
https://github.com/psviderski/uncloud.git
synced 2026-11-11 13:14:17 +00:00
Compare commits
77
Commits
f68c9859d9
...
nightly
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffd93096f1 | ||
|
|
24b4837d08 | ||
|
|
6a64bddf27 | ||
|
|
d1d496cf32 | ||
|
|
286d0e6afb | ||
|
|
cef6916544 | ||
|
|
68d2bd5656 | ||
|
|
99c928f490 | ||
|
|
e81115ef37 | ||
|
|
22b1c68798 | ||
|
|
76b04dd56c | ||
|
|
78facdfcc6 | ||
|
|
7b48ac5a4d | ||
|
|
6c7ddeb82f | ||
|
|
ff6ac45e1e | ||
|
|
c1a7f84dad | ||
|
|
81f30aac26 | ||
|
|
b214d23f21 | ||
|
|
965879b69f | ||
|
|
d66ec2bada | ||
|
|
20e2e97c36 | ||
|
|
e9eab32e6d | ||
|
|
7c6d1be8a7 | ||
|
|
a2987d3ff2 | ||
|
|
3351111feb | ||
|
|
bd695f65a3 | ||
|
|
7e8736fa99 | ||
|
|
8160c9b9d0 | ||
|
|
80d2a323c6 | ||
|
|
5d640b9a57 | ||
|
|
ba224bb5be | ||
|
|
8ddbe3c606 | ||
|
|
ce1c6bd05a | ||
|
|
11e1200fe8 | ||
|
|
0cdf721a6a | ||
|
|
5e26cfbead | ||
|
|
409dc3bfe3 | ||
|
|
5fa236871f | ||
|
|
e3479409b1 | ||
|
|
a1b6b31e9c | ||
|
|
d04772dacd | ||
|
|
87691ec029 | ||
|
|
7ecf63169b | ||
|
|
c58108aa82 | ||
|
|
5d7952952a | ||
|
|
87e99ae369 | ||
|
|
f78a8ca8b5 | ||
|
|
e4bd1ad443 | ||
|
|
0e53f505c0 | ||
|
|
9c21ae6b08 | ||
|
|
70de7b1b8f | ||
|
|
af277639dd | ||
|
|
25ca0a57e8 | ||
|
|
4dae187aa5 | ||
|
|
930e7ea637 | ||
|
|
e50c5fbe57 | ||
|
|
e4455e936a | ||
|
|
ebecf334d7 | ||
|
|
417e402c73 | ||
|
|
9ee3ca15eb | ||
|
|
3482eeefab | ||
|
|
ace8cbf974 | ||
|
|
45d33d87dd | ||
|
|
5c33e4be86 | ||
|
|
88dda435e7 | ||
|
|
ae940c3003 | ||
|
|
e525bdeb7c | ||
|
|
bc09954cbd | ||
|
|
b28c458029 | ||
|
|
3fc1cddf01 | ||
|
|
54a7328a34 | ||
|
|
47a4a9158e | ||
|
|
ad2e1a06d5 | ||
|
|
81d36edc66 | ||
|
|
5fb80e2071 | ||
|
|
d1fe23dec0 | ||
|
|
cec3c75a10 |
@@ -18,3 +18,6 @@ indent_size = 4
|
|||||||
[*.sh]
|
[*.sh]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
|
[release-notes-*.md]
|
||||||
|
max_line_length = off
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ on:
|
|||||||
- "**.proto"
|
- "**.proto"
|
||||||
- "go.*"
|
- "go.*"
|
||||||
- "Makefile"
|
- "Makefile"
|
||||||
|
- "mise.toml"
|
||||||
- "scripts/**"
|
- "scripts/**"
|
||||||
- "test/**"
|
- "test/**"
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
env:
|
env:
|
||||||
MISE_VERSION: "2026.3.7"
|
MISE_VERSION: "2026.10.22"
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -42,9 +43,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate docs
|
- name: Generate docs
|
||||||
run: |
|
run: |
|
||||||
make cli-docs
|
mise run docs:cli
|
||||||
git diff --exit-code ||
|
git diff --exit-code ||
|
||||||
(echo "Documentation is out of date. Please run 'make cli-docs' and commit the changes." && exit 1)
|
(echo "Documentation is out of date. Please run 'mise run docs:cli' and commit the changes." && exit 1)
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
@@ -70,7 +71,7 @@ jobs:
|
|||||||
version: ${{ env.MISE_VERSION }}
|
version: ${{ env.MISE_VERSION }}
|
||||||
|
|
||||||
# TODO: figure out how to properly lock all platforms in the lockfile and remove this hack.
|
# TODO: figure out how to properly lock all platforms in the lockfile and remove this hack.
|
||||||
# `mise lock` (mise run mise:lock) doesn't generate checksums for some of the tools, e.g. protoc*
|
# `mise lock` doesn't generate checksums for some of the tools, e.g. protoc*
|
||||||
- name: Reset changes in mise.lock after fresh mise install
|
- name: Reset changes in mise.lock after fresh mise install
|
||||||
run: git checkout mise.lock
|
run: git checkout mise.lock
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
env:
|
env:
|
||||||
MISE_VERSION: "2026.3.7"
|
MISE_VERSION: "2026.10.22"
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -30,6 +30,6 @@ jobs:
|
|||||||
run: git checkout mise.lock
|
run: git checkout mise.lock
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: make lint
|
run: mise run lint
|
||||||
|
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MISE_VERSION: "2026.3.7"
|
MISE_VERSION: "2026.10.22"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
nightly:
|
nightly:
|
||||||
|
|||||||
@@ -44,3 +44,4 @@ node_modules/
|
|||||||
.devcontainer/
|
.devcontainer/
|
||||||
|
|
||||||
.zed/
|
.zed/
|
||||||
|
research/
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
packages:
|
packages:
|
||||||
|
github.com/docker/docker/client:
|
||||||
|
config:
|
||||||
|
dir: internal/machine/docker
|
||||||
|
pkgname: docker
|
||||||
|
interfaces:
|
||||||
|
APIClient:
|
||||||
github.com/psviderski/uncloud/internal/machine/caddyconfig:
|
github.com/psviderski/uncloud/internal/machine/caddyconfig:
|
||||||
interfaces:
|
interfaces:
|
||||||
CaddyfileValidator:
|
CaddyfileValidator:
|
||||||
|
|||||||
@@ -98,33 +98,6 @@ management.
|
|||||||
|
|
||||||
- **`misc/`**: Design documents and guides
|
- **`misc/`**: Design documents and guides
|
||||||
|
|
||||||
## Key Technologies
|
|
||||||
|
|
||||||
### Core Dependencies
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Networking and orchestration
|
|
||||||
github.com/docker/docker // Docker API client
|
|
||||||
github.com/docker/compose/v2 // Docker Compose integration
|
|
||||||
golang.zx2c4.com/wireguard // WireGuard implementation
|
|
||||||
github.com/hashicorp/serf // Gossip protocol
|
|
||||||
|
|
||||||
// State management
|
|
||||||
github.com/ipfs/go-ds-crdt // CRDT distributed storage
|
|
||||||
github.com/dgraph-io/badger/v3 // Embedded database
|
|
||||||
|
|
||||||
// Web proxy
|
|
||||||
github.com/caddyserver/caddy/v2 // HTTP server and reverse proxy
|
|
||||||
|
|
||||||
// CLI and UX
|
|
||||||
github.com/spf13/cobra // CLI framework
|
|
||||||
github.com/charmbracelet/huh // Interactive forms
|
|
||||||
|
|
||||||
// gRPC and networking
|
|
||||||
google.golang.org/grpc // gRPC framework
|
|
||||||
github.com/siderolabs/grpc-proxy // gRPC proxy for forwarding
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
### Build and Development
|
### Build and Development
|
||||||
@@ -135,16 +108,18 @@ go build -o uc ./cmd/uc
|
|||||||
go build -o uncloudd ./cmd/uncloudd
|
go build -o uncloudd ./cmd/uncloudd
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Make Targets
|
### Formatting and Linting
|
||||||
|
|
||||||
|
- `mise run lint`: Check Go code and formatting without changing files
|
||||||
|
- `mise run lint:fix`: Apply formatting and available lint fixes
|
||||||
|
|
||||||
|
### Other Development Tasks
|
||||||
|
|
||||||
- `proto`: Generate protobuf code
|
- `proto`: Generate protobuf code
|
||||||
- `ucind-cluster`: Create development cluster
|
- `ucind-cluster`: Create development cluster
|
||||||
- `update-dev`: Deploy to development machines
|
- `update-dev`: Deploy to development machines
|
||||||
- `demo-reset`: Reset demo environment
|
- `demo-reset`: Reset demo environment
|
||||||
- `fmt`: Format code
|
|
||||||
- `test`: Run all tests
|
- `test`: Run all tests
|
||||||
- `lint`: Lint the code using golangci-lint
|
|
||||||
- `lint-and-fix`: Lint the code and fix issues whenever possible
|
|
||||||
|
|
||||||
## CLI Commands Structure
|
## CLI Commands Structure
|
||||||
|
|
||||||
|
|||||||
+34
-5
@@ -35,10 +35,11 @@ Build the CLI:
|
|||||||
go build -o uc ./cmd/uc
|
go build -o uc ./cmd/uc
|
||||||
```
|
```
|
||||||
|
|
||||||
Or build and run the CLI with a single command:
|
Or build and run the CLI with mise. It forwards arguments, including help flags, directly to the CLI:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
go run ./cmd/uc --help
|
mise uc --help
|
||||||
|
mise uc image rm -h
|
||||||
```
|
```
|
||||||
|
|
||||||
The Uncloud daemon (`uncloudd`) only supports Linux, so you need to cross-compile it if you're developing on macOS or
|
The Uncloud daemon (`uncloudd`) only supports Linux, so you need to cross-compile it if you're developing on macOS or
|
||||||
@@ -99,10 +100,16 @@ mise run dev:reset user@host1 user@host2
|
|||||||
|
|
||||||
## Linting and formatting
|
## Linting and formatting
|
||||||
|
|
||||||
Lint and format the code:
|
Check the code and formatting without changing files:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
make lint-and-fix
|
mise run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
Apply formatting and available lint fixes:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mise run lint:fix
|
||||||
```
|
```
|
||||||
|
|
||||||
## Code generation
|
## Code generation
|
||||||
@@ -116,9 +123,31 @@ mise run proto
|
|||||||
Regenerate mocks:
|
Regenerate mocks:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
make mocks
|
mise run mocks
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Regenerate the CLI reference docs after changing commands or flags:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mise run docs:cli
|
||||||
|
```
|
||||||
|
|
||||||
|
To preview the documentation website, install its dependencies from the project root:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm ci --prefix website
|
||||||
|
```
|
||||||
|
|
||||||
|
Then start the local development server:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mise run docs:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The server opens the site (http://localhost:3000/) in your browser and reloads it as you edit files in `website/`.
|
||||||
|
|
||||||
## Uncloud in Docker (UCinD)
|
## Uncloud in Docker (UCinD)
|
||||||
|
|
||||||
The [`ucind`](./cmd/ucind) CLI lets you run Uncloud clusters locally using Docker containers instead of real machines.
|
The [`ucind`](./cmd/ucind) CLI lets you run Uncloud clusters locally using Docker containers instead of real machines.
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ corrosion-multiarch-image-push:
|
|||||||
ucind-multiarch-image-push:
|
ucind-multiarch-image-push:
|
||||||
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(UCIND_IMAGE)" --target ucind .
|
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(UCIND_IMAGE)" --target ucind .
|
||||||
|
|
||||||
.PHONY: mocks
|
|
||||||
mocks:
|
|
||||||
@mockery
|
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
ifeq ($(TEST_NAME),)
|
ifeq ($(TEST_NAME),)
|
||||||
@@ -51,25 +47,3 @@ test-clean:
|
|||||||
else \
|
else \
|
||||||
echo "Nothing to clean."; \
|
echo "Nothing to clean."; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
.PHONY: vet
|
|
||||||
vet:
|
|
||||||
go vet ./...
|
|
||||||
|
|
||||||
.PHONY: format fmt
|
|
||||||
format fmt:
|
|
||||||
GOOS=linux golangci-lint fmt
|
|
||||||
|
|
||||||
LINT_TARGETS := lint lint-and-fix
|
|
||||||
.PHONY: $(LINT_TARGETS) _lint
|
|
||||||
$(LINT_TARGETS): _lint
|
|
||||||
lint: ARGS=
|
|
||||||
lint-and-fix: ARGS=--fix
|
|
||||||
_lint:
|
|
||||||
# Explicitly set OS to Linux to not skip *_linux.go files when running on macOS.
|
|
||||||
# Uncloud daemon won't likely support OS other than Linux anytime soon, so for now we can rely on that.
|
|
||||||
GOOS=linux golangci-lint run $(ARGS)
|
|
||||||
|
|
||||||
.PHONY: cli-docs
|
|
||||||
cli-docs:
|
|
||||||
go run ./cmd/uc docs
|
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ complexity of Kubernetes.
|
|||||||
[Uncloud DNS](https://github.com/psviderski/uncloud-dns) service.
|
[Uncloud DNS](https://github.com/psviderski/uncloud-dns) service.
|
||||||
* **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's
|
* **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's
|
||||||
Encrypt.
|
Encrypt.
|
||||||
|
* **Cluster storage for TLS certificates**: [Cluster storage](https://github.com/unlabs-dev/caddy-uncloud) for Caddy
|
||||||
|
shares certificates and ACME challenge tokens across machines and coordinates issuance.
|
||||||
* **Docker-like CLI**: Familiar commands for managing both infrastructure and applications.
|
* **Docker-like CLI**: Familiar commands for managing both infrastructure and applications.
|
||||||
* **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster.
|
* **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster.
|
||||||
|
|
||||||
|
|||||||
+30
-17
@@ -27,10 +27,12 @@ type GetCaddyConfigResponse struct {
|
|||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
// The generated Caddyfile content.
|
// The saved Caddyfile content.
|
||||||
Caddyfile string `protobuf:"bytes,1,opt,name=caddyfile,proto3" json:"caddyfile,omitempty"`
|
Caddyfile string `protobuf:"bytes,1,opt,name=caddyfile,proto3" json:"caddyfile,omitempty"`
|
||||||
// Timestamp when the config was last modified.
|
// Timestamp when the config was last modified.
|
||||||
ModifiedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=modified_at,json=modifiedAt,proto3" json:"modified_at,omitempty"`
|
ModifiedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=modified_at,json=modifiedAt,proto3" json:"modified_at,omitempty"`
|
||||||
|
// Error from the latest unsuccessful reconciliation attempt.
|
||||||
|
LastReconciliationError string `protobuf:"bytes,3,opt,name=last_reconciliation_error,json=lastReconciliationError,proto3" json:"last_reconciliation_error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *GetCaddyConfigResponse) Reset() {
|
func (x *GetCaddyConfigResponse) Reset() {
|
||||||
@@ -79,6 +81,13 @@ func (x *GetCaddyConfigResponse) GetModifiedAt() *timestamppb.Timestamp {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *GetCaddyConfigResponse) GetLastReconciliationError() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.LastReconciliationError
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
var File_api_pb_caddy_proto protoreflect.FileDescriptor
|
var File_api_pb_caddy_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
var file_api_pb_caddy_proto_rawDesc = []byte{
|
var file_api_pb_caddy_proto_rawDesc = []byte{
|
||||||
@@ -87,22 +96,26 @@ var file_api_pb_caddy_proto_rawDesc = []byte{
|
|||||||
0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79,
|
0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79,
|
||||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
|
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
|
||||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
||||||
0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x73, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x43, 0x61,
|
0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xaf, 0x01, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x43,
|
||||||
0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
0x61, 0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||||
0x65, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x01,
|
0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65, 0x18,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65, 0x12,
|
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65,
|
||||||
0x3b, 0x0a, 0x0b, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02,
|
0x12, 0x3b, 0x0a, 0x0b, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18,
|
||||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
|
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
||||||
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
|
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
||||||
0x52, 0x0a, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x74, 0x32, 0x49, 0x0a, 0x05,
|
0x70, 0x52, 0x0a, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x74, 0x12, 0x3a, 0x0a,
|
||||||
0x43, 0x61, 0x64, 0x64, 0x79, 0x12, 0x40, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66,
|
0x19, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x69, 0x61,
|
||||||
0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69,
|
0x52, 0x17, 0x6c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x69, 0x61,
|
||||||
0x2e, 0x47, 0x65, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
|
0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x32, 0x49, 0x0a, 0x05, 0x43, 0x61, 0x64,
|
||||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75,
|
0x64, 0x79, 0x12, 0x40, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
|
||||||
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69,
|
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
||||||
0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62,
|
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65,
|
||||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70,
|
||||||
|
0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
|
||||||
|
0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e,
|
||||||
|
0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
|
||||||
|
0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
+3
-1
@@ -13,8 +13,10 @@ service Caddy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message GetCaddyConfigResponse {
|
message GetCaddyConfigResponse {
|
||||||
// The generated Caddyfile content.
|
// The saved Caddyfile content.
|
||||||
string caddyfile = 1;
|
string caddyfile = 1;
|
||||||
// Timestamp when the config was last modified.
|
// Timestamp when the config was last modified.
|
||||||
google.protobuf.Timestamp modified_at = 2;
|
google.protobuf.Timestamp modified_at = 2;
|
||||||
|
// Error from the latest unsuccessful reconciliation attempt.
|
||||||
|
string last_reconciliation_error = 3;
|
||||||
}
|
}
|
||||||
|
|||||||
+205
-46
@@ -446,6 +446,120 @@ func (x *StatCaddyStorageResponse) GetIsTerminal() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ListCertificatesResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Certificates []*IssuedCertificate `protobuf:"bytes,1,rep,name=certificates,proto3" json:"certificates,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListCertificatesResponse) Reset() {
|
||||||
|
*x = ListCertificatesResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_api_pb_caddy_storage_proto_msgTypes[8]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListCertificatesResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ListCertificatesResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ListCertificatesResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_api_pb_caddy_storage_proto_msgTypes[8]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ListCertificatesResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ListCertificatesResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_api_pb_caddy_storage_proto_rawDescGZIP(), []int{8}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListCertificatesResponse) GetCertificates() []*IssuedCertificate {
|
||||||
|
if x != nil {
|
||||||
|
return x.Certificates
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type IssuedCertificate struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// Subject Alternative Name (SAN) of the certificate. Caddy doesn't issue certificates with multiple SANs.
|
||||||
|
San string `protobuf:"bytes,1,opt,name=san,proto3" json:"san,omitempty"`
|
||||||
|
// The PEM-encoding of DER-encoded ASN.1 data for the cert or chain, leaf first.
|
||||||
|
Chain []byte `protobuf:"bytes,2,opt,name=chain,proto3" json:"chain,omitempty"`
|
||||||
|
// Any extra information associated with the certificate, usually provided by the issuer implementation.
|
||||||
|
// JSON-encoded, may be absent or null.
|
||||||
|
IssuerData []byte `protobuf:"bytes,3,opt,name=issuer_data,json=issuerData,proto3" json:"issuer_data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IssuedCertificate) Reset() {
|
||||||
|
*x = IssuedCertificate{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_api_pb_caddy_storage_proto_msgTypes[9]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IssuedCertificate) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*IssuedCertificate) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *IssuedCertificate) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_api_pb_caddy_storage_proto_msgTypes[9]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use IssuedCertificate.ProtoReflect.Descriptor instead.
|
||||||
|
func (*IssuedCertificate) Descriptor() ([]byte, []int) {
|
||||||
|
return file_api_pb_caddy_storage_proto_rawDescGZIP(), []int{9}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IssuedCertificate) GetSan() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.San
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IssuedCertificate) GetChain() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Chain
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IssuedCertificate) GetIssuerData() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.IssuerData
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var File_api_pb_caddy_storage_proto protoreflect.FileDescriptor
|
var File_api_pb_caddy_storage_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
var file_api_pb_caddy_storage_proto_rawDesc = []byte{
|
var file_api_pb_caddy_storage_proto_rawDesc = []byte{
|
||||||
@@ -492,32 +606,48 @@ var file_api_pb_caddy_storage_proto_rawDesc = []byte{
|
|||||||
0x65, 0x64, 0x41, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01,
|
0x65, 0x64, 0x41, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01,
|
||||||
0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x74,
|
0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x74,
|
||||||
0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69,
|
0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69,
|
||||||
0x73, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x32, 0xdf, 0x02, 0x0a, 0x0c, 0x43, 0x61,
|
0x73, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x22, 0x56, 0x0a, 0x18, 0x4c, 0x69, 0x73,
|
||||||
0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x12, 0x3e, 0x0a, 0x05, 0x53, 0x74,
|
0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73,
|
||||||
0x6f, 0x72, 0x65, 0x12, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x43,
|
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69,
|
||||||
|
0x63, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70,
|
||||||
|
0x69, 0x2e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63,
|
||||||
|
0x61, 0x74, 0x65, 0x52, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65,
|
||||||
|
0x73, 0x22, 0x5c, 0x0a, 0x11, 0x49, 0x73, 0x73, 0x75, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69,
|
||||||
|
0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x61, 0x6e, 0x18, 0x01, 0x20,
|
||||||
|
0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x61, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x68, 0x61, 0x69,
|
||||||
|
0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x1f,
|
||||||
|
0x0a, 0x0b, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20,
|
||||||
|
0x01, 0x28, 0x0c, 0x52, 0x0a, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x32,
|
||||||
|
0xaa, 0x03, 0x0a, 0x0c, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
|
||||||
|
0x12, 0x3e, 0x0a, 0x05, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
|
0x53, 0x74, 0x6f, 0x72, 0x65, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67,
|
||||||
|
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||||
|
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
||||||
|
0x12, 0x43, 0x0a, 0x04, 0x4c, 0x6f, 0x61, 0x64, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c,
|
||||||
|
0x6f, 0x61, 0x64, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52,
|
||||||
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x61,
|
||||||
|
0x64, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73,
|
||||||
|
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12,
|
||||||
|
0x1e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x61, 0x64, 0x64,
|
||||||
|
0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||||
|
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
||||||
|
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12,
|
||||||
|
0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53,
|
||||||
|
0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e,
|
||||||
|
0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f,
|
||||||
|
0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x04,
|
||||||
|
0x53, 0x74, 0x61, 0x74, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x43,
|
||||||
0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
|
0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||||
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x43, 0x61, 0x64,
|
||||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x04, 0x4c, 0x6f,
|
0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||||
0x61, 0x64, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x43, 0x61, 0x64,
|
0x65, 0x12, 0x49, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69,
|
||||||
0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
||||||
0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x43, 0x61, 0x64, 0x64, 0x79,
|
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e,
|
||||||
0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63,
|
||||||
0x40, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1e, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24,
|
||||||
0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61,
|
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64,
|
||||||
0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70,
|
||||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
|
0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
0x79, 0x12, 0x43, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
|
||||||
0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
|
|
||||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69,
|
|
||||||
0x73, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65,
|
|
||||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x04, 0x53, 0x74, 0x61, 0x74, 0x12, 0x1c,
|
|
||||||
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74,
|
|
||||||
0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61,
|
|
||||||
0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72,
|
|
||||||
0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67,
|
|
||||||
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65,
|
|
||||||
0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70, 0x69,
|
|
||||||
0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -532,7 +662,7 @@ func file_api_pb_caddy_storage_proto_rawDescGZIP() []byte {
|
|||||||
return file_api_pb_caddy_storage_proto_rawDescData
|
return file_api_pb_caddy_storage_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_api_pb_caddy_storage_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
|
var file_api_pb_caddy_storage_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
|
||||||
var file_api_pb_caddy_storage_proto_goTypes = []any{
|
var file_api_pb_caddy_storage_proto_goTypes = []any{
|
||||||
(*StoreCaddyStorageRequest)(nil), // 0: api.StoreCaddyStorageRequest
|
(*StoreCaddyStorageRequest)(nil), // 0: api.StoreCaddyStorageRequest
|
||||||
(*LoadCaddyStorageRequest)(nil), // 1: api.LoadCaddyStorageRequest
|
(*LoadCaddyStorageRequest)(nil), // 1: api.LoadCaddyStorageRequest
|
||||||
@@ -542,27 +672,32 @@ var file_api_pb_caddy_storage_proto_goTypes = []any{
|
|||||||
(*ListCaddyStorageResponse)(nil), // 5: api.ListCaddyStorageResponse
|
(*ListCaddyStorageResponse)(nil), // 5: api.ListCaddyStorageResponse
|
||||||
(*StatCaddyStorageRequest)(nil), // 6: api.StatCaddyStorageRequest
|
(*StatCaddyStorageRequest)(nil), // 6: api.StatCaddyStorageRequest
|
||||||
(*StatCaddyStorageResponse)(nil), // 7: api.StatCaddyStorageResponse
|
(*StatCaddyStorageResponse)(nil), // 7: api.StatCaddyStorageResponse
|
||||||
(*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp
|
(*ListCertificatesResponse)(nil), // 8: api.ListCertificatesResponse
|
||||||
(*emptypb.Empty)(nil), // 9: google.protobuf.Empty
|
(*IssuedCertificate)(nil), // 9: api.IssuedCertificate
|
||||||
|
(*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
|
||||||
|
(*emptypb.Empty)(nil), // 11: google.protobuf.Empty
|
||||||
}
|
}
|
||||||
var file_api_pb_caddy_storage_proto_depIdxs = []int32{
|
var file_api_pb_caddy_storage_proto_depIdxs = []int32{
|
||||||
8, // 0: api.LoadCaddyStorageResponse.updated_at:type_name -> google.protobuf.Timestamp
|
10, // 0: api.LoadCaddyStorageResponse.updated_at:type_name -> google.protobuf.Timestamp
|
||||||
8, // 1: api.StatCaddyStorageResponse.updated_at:type_name -> google.protobuf.Timestamp
|
10, // 1: api.StatCaddyStorageResponse.updated_at:type_name -> google.protobuf.Timestamp
|
||||||
0, // 2: api.CaddyStorage.Store:input_type -> api.StoreCaddyStorageRequest
|
9, // 2: api.ListCertificatesResponse.certificates:type_name -> api.IssuedCertificate
|
||||||
1, // 3: api.CaddyStorage.Load:input_type -> api.LoadCaddyStorageRequest
|
0, // 3: api.CaddyStorage.Store:input_type -> api.StoreCaddyStorageRequest
|
||||||
3, // 4: api.CaddyStorage.Delete:input_type -> api.DeleteCaddyStorageRequest
|
1, // 4: api.CaddyStorage.Load:input_type -> api.LoadCaddyStorageRequest
|
||||||
4, // 5: api.CaddyStorage.List:input_type -> api.ListCaddyStorageRequest
|
3, // 5: api.CaddyStorage.Delete:input_type -> api.DeleteCaddyStorageRequest
|
||||||
6, // 6: api.CaddyStorage.Stat:input_type -> api.StatCaddyStorageRequest
|
4, // 6: api.CaddyStorage.List:input_type -> api.ListCaddyStorageRequest
|
||||||
9, // 7: api.CaddyStorage.Store:output_type -> google.protobuf.Empty
|
6, // 7: api.CaddyStorage.Stat:input_type -> api.StatCaddyStorageRequest
|
||||||
2, // 8: api.CaddyStorage.Load:output_type -> api.LoadCaddyStorageResponse
|
11, // 8: api.CaddyStorage.ListCertificates:input_type -> google.protobuf.Empty
|
||||||
9, // 9: api.CaddyStorage.Delete:output_type -> google.protobuf.Empty
|
11, // 9: api.CaddyStorage.Store:output_type -> google.protobuf.Empty
|
||||||
5, // 10: api.CaddyStorage.List:output_type -> api.ListCaddyStorageResponse
|
2, // 10: api.CaddyStorage.Load:output_type -> api.LoadCaddyStorageResponse
|
||||||
7, // 11: api.CaddyStorage.Stat:output_type -> api.StatCaddyStorageResponse
|
11, // 11: api.CaddyStorage.Delete:output_type -> google.protobuf.Empty
|
||||||
7, // [7:12] is the sub-list for method output_type
|
5, // 12: api.CaddyStorage.List:output_type -> api.ListCaddyStorageResponse
|
||||||
2, // [2:7] is the sub-list for method input_type
|
7, // 13: api.CaddyStorage.Stat:output_type -> api.StatCaddyStorageResponse
|
||||||
2, // [2:2] is the sub-list for extension type_name
|
8, // 14: api.CaddyStorage.ListCertificates:output_type -> api.ListCertificatesResponse
|
||||||
2, // [2:2] is the sub-list for extension extendee
|
9, // [9:15] is the sub-list for method output_type
|
||||||
0, // [0:2] is the sub-list for field type_name
|
3, // [3:9] is the sub-list for method input_type
|
||||||
|
3, // [3:3] is the sub-list for extension type_name
|
||||||
|
3, // [3:3] is the sub-list for extension extendee
|
||||||
|
0, // [0:3] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_api_pb_caddy_storage_proto_init() }
|
func init() { file_api_pb_caddy_storage_proto_init() }
|
||||||
@@ -667,6 +802,30 @@ func file_api_pb_caddy_storage_proto_init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
file_api_pb_caddy_storage_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*ListCertificatesResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_api_pb_caddy_storage_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*IssuedCertificate); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
@@ -674,7 +833,7 @@ func file_api_pb_caddy_storage_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_api_pb_caddy_storage_proto_rawDesc,
|
RawDescriptor: file_api_pb_caddy_storage_proto_rawDesc,
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 8,
|
NumMessages: 10,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ service CaddyStorage {
|
|||||||
rpc Delete(DeleteCaddyStorageRequest) returns (google.protobuf.Empty);
|
rpc Delete(DeleteCaddyStorageRequest) returns (google.protobuf.Empty);
|
||||||
rpc List(ListCaddyStorageRequest) returns (ListCaddyStorageResponse);
|
rpc List(ListCaddyStorageRequest) returns (ListCaddyStorageResponse);
|
||||||
rpc Stat(StatCaddyStorageRequest) returns (StatCaddyStorageResponse);
|
rpc Stat(StatCaddyStorageRequest) returns (StatCaddyStorageResponse);
|
||||||
|
|
||||||
|
// ListCertificates lists certificates issued by Caddy and stored in the cluster store.
|
||||||
|
// Unreadable entries and invalid resource metadata are skipped. Chains and issuer data are returned as stored.
|
||||||
|
rpc ListCertificates(google.protobuf.Empty) returns (ListCertificatesResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
message StoreCaddyStorageRequest {
|
message StoreCaddyStorageRequest {
|
||||||
@@ -57,3 +61,17 @@ message StatCaddyStorageResponse {
|
|||||||
int64 size = 3;
|
int64 size = 3;
|
||||||
bool is_terminal = 4;
|
bool is_terminal = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ListCertificatesResponse {
|
||||||
|
repeated IssuedCertificate certificates = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message IssuedCertificate {
|
||||||
|
// Subject Alternative Name (SAN) of the certificate. Caddy doesn't issue certificates with multiple SANs.
|
||||||
|
string san = 1;
|
||||||
|
// The PEM-encoding of DER-encoded ASN.1 data for the cert or chain, leaf first.
|
||||||
|
bytes chain = 2;
|
||||||
|
// Any extra information associated with the certificate, usually provided by the issuer implementation.
|
||||||
|
// JSON-encoded, may be absent or null.
|
||||||
|
bytes issuer_data = 3;
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const (
|
|||||||
CaddyStorage_Delete_FullMethodName = "/api.CaddyStorage/Delete"
|
CaddyStorage_Delete_FullMethodName = "/api.CaddyStorage/Delete"
|
||||||
CaddyStorage_List_FullMethodName = "/api.CaddyStorage/List"
|
CaddyStorage_List_FullMethodName = "/api.CaddyStorage/List"
|
||||||
CaddyStorage_Stat_FullMethodName = "/api.CaddyStorage/Stat"
|
CaddyStorage_Stat_FullMethodName = "/api.CaddyStorage/Stat"
|
||||||
|
CaddyStorage_ListCertificates_FullMethodName = "/api.CaddyStorage/ListCertificates"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CaddyStorageClient is the client API for CaddyStorage service.
|
// CaddyStorageClient is the client API for CaddyStorage service.
|
||||||
@@ -42,6 +43,9 @@ type CaddyStorageClient interface {
|
|||||||
Delete(ctx context.Context, in *DeleteCaddyStorageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
Delete(ctx context.Context, in *DeleteCaddyStorageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
List(ctx context.Context, in *ListCaddyStorageRequest, opts ...grpc.CallOption) (*ListCaddyStorageResponse, error)
|
List(ctx context.Context, in *ListCaddyStorageRequest, opts ...grpc.CallOption) (*ListCaddyStorageResponse, error)
|
||||||
Stat(ctx context.Context, in *StatCaddyStorageRequest, opts ...grpc.CallOption) (*StatCaddyStorageResponse, error)
|
Stat(ctx context.Context, in *StatCaddyStorageRequest, opts ...grpc.CallOption) (*StatCaddyStorageResponse, error)
|
||||||
|
// ListCertificates lists certificates issued by Caddy and stored in the cluster store.
|
||||||
|
// Unreadable entries and invalid resource metadata are skipped. Chains and issuer data are returned as stored.
|
||||||
|
ListCertificates(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListCertificatesResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type caddyStorageClient struct {
|
type caddyStorageClient struct {
|
||||||
@@ -102,6 +106,16 @@ func (c *caddyStorageClient) Stat(ctx context.Context, in *StatCaddyStorageReque
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *caddyStorageClient) ListCertificates(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListCertificatesResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListCertificatesResponse)
|
||||||
|
err := c.cc.Invoke(ctx, CaddyStorage_ListCertificates_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CaddyStorageServer is the server API for CaddyStorage service.
|
// CaddyStorageServer is the server API for CaddyStorage service.
|
||||||
// All implementations must embed UnimplementedCaddyStorageServer
|
// All implementations must embed UnimplementedCaddyStorageServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@@ -117,6 +131,9 @@ type CaddyStorageServer interface {
|
|||||||
Delete(context.Context, *DeleteCaddyStorageRequest) (*emptypb.Empty, error)
|
Delete(context.Context, *DeleteCaddyStorageRequest) (*emptypb.Empty, error)
|
||||||
List(context.Context, *ListCaddyStorageRequest) (*ListCaddyStorageResponse, error)
|
List(context.Context, *ListCaddyStorageRequest) (*ListCaddyStorageResponse, error)
|
||||||
Stat(context.Context, *StatCaddyStorageRequest) (*StatCaddyStorageResponse, error)
|
Stat(context.Context, *StatCaddyStorageRequest) (*StatCaddyStorageResponse, error)
|
||||||
|
// ListCertificates lists certificates issued by Caddy and stored in the cluster store.
|
||||||
|
// Unreadable entries and invalid resource metadata are skipped. Chains and issuer data are returned as stored.
|
||||||
|
ListCertificates(context.Context, *emptypb.Empty) (*ListCertificatesResponse, error)
|
||||||
mustEmbedUnimplementedCaddyStorageServer()
|
mustEmbedUnimplementedCaddyStorageServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +159,9 @@ func (UnimplementedCaddyStorageServer) List(context.Context, *ListCaddyStorageRe
|
|||||||
func (UnimplementedCaddyStorageServer) Stat(context.Context, *StatCaddyStorageRequest) (*StatCaddyStorageResponse, error) {
|
func (UnimplementedCaddyStorageServer) Stat(context.Context, *StatCaddyStorageRequest) (*StatCaddyStorageResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method Stat not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method Stat not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedCaddyStorageServer) ListCertificates(context.Context, *emptypb.Empty) (*ListCertificatesResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ListCertificates not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedCaddyStorageServer) mustEmbedUnimplementedCaddyStorageServer() {}
|
func (UnimplementedCaddyStorageServer) mustEmbedUnimplementedCaddyStorageServer() {}
|
||||||
func (UnimplementedCaddyStorageServer) testEmbeddedByValue() {}
|
func (UnimplementedCaddyStorageServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@@ -253,6 +273,24 @@ func _CaddyStorage_Stat_Handler(srv interface{}, ctx context.Context, dec func(i
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _CaddyStorage_ListCertificates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(emptypb.Empty)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(CaddyStorageServer).ListCertificates(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: CaddyStorage_ListCertificates_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(CaddyStorageServer).ListCertificates(ctx, req.(*emptypb.Empty))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// CaddyStorage_ServiceDesc is the grpc.ServiceDesc for CaddyStorage service.
|
// CaddyStorage_ServiceDesc is the grpc.ServiceDesc for CaddyStorage service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@@ -280,6 +318,10 @@ var CaddyStorage_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "Stat",
|
MethodName: "Stat",
|
||||||
Handler: _CaddyStorage_Stat_Handler,
|
Handler: _CaddyStorage_Stat_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ListCertificates",
|
||||||
|
Handler: _CaddyStorage_ListCertificates_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "api/pb/caddy_storage.proto",
|
Metadata: "api/pb/caddy_storage.proto",
|
||||||
|
|||||||
+178
-95
@@ -124,7 +124,7 @@ func (x DNSRecord_RecordType) Number() protoreflect.EnumNumber {
|
|||||||
|
|
||||||
// Deprecated: Use DNSRecord_RecordType.Descriptor instead.
|
// Deprecated: Use DNSRecord_RecordType.Descriptor instead.
|
||||||
func (DNSRecord_RecordType) EnumDescriptor() ([]byte, []int) {
|
func (DNSRecord_RecordType) EnumDescriptor() ([]byte, []int) {
|
||||||
return file_api_pb_cluster_proto_rawDescGZIP(), []int{9, 0}
|
return file_api_pb_cluster_proto_rawDescGZIP(), []int{10, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddMachineRequest struct {
|
type AddMachineRequest struct {
|
||||||
@@ -392,6 +392,9 @@ type Domain struct {
|
|||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
// Whether the domain is reserved in Uncloud DNS. Older daemons (<0.21) omit this field
|
||||||
|
// and only support reserved domains, so clients should treat absence as true.
|
||||||
|
Reserved *bool `protobuf:"varint,2,opt,name=reserved,proto3,oneof" json:"reserved,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Domain) Reset() {
|
func (x *Domain) Reset() {
|
||||||
@@ -433,6 +436,13 @@ func (x *Domain) GetName() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *Domain) GetReserved() bool {
|
||||||
|
if x != nil && x.Reserved != nil {
|
||||||
|
return *x.Reserved
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type ReserveDomainRequest struct {
|
type ReserveDomainRequest struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -480,6 +490,54 @@ func (x *ReserveDomainRequest) GetEndpoint() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SetDomainRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// An externally managed domain name. Empty clears a manually set domain.
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SetDomainRequest) Reset() {
|
||||||
|
*x = SetDomainRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_api_pb_cluster_proto_msgTypes[7]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SetDomainRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SetDomainRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *SetDomainRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_api_pb_cluster_proto_msgTypes[7]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use SetDomainRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*SetDomainRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_api_pb_cluster_proto_rawDescGZIP(), []int{7}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SetDomainRequest) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type CreateDomainRecordsRequest struct {
|
type CreateDomainRecordsRequest struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -491,7 +549,7 @@ type CreateDomainRecordsRequest struct {
|
|||||||
func (x *CreateDomainRecordsRequest) Reset() {
|
func (x *CreateDomainRecordsRequest) Reset() {
|
||||||
*x = CreateDomainRecordsRequest{}
|
*x = CreateDomainRecordsRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_api_pb_cluster_proto_msgTypes[7]
|
mi := &file_api_pb_cluster_proto_msgTypes[8]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -504,7 +562,7 @@ func (x *CreateDomainRecordsRequest) String() string {
|
|||||||
func (*CreateDomainRecordsRequest) ProtoMessage() {}
|
func (*CreateDomainRecordsRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
|
func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_api_pb_cluster_proto_msgTypes[7]
|
mi := &file_api_pb_cluster_proto_msgTypes[8]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -517,7 +575,7 @@ func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use CreateDomainRecordsRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use CreateDomainRecordsRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*CreateDomainRecordsRequest) Descriptor() ([]byte, []int) {
|
func (*CreateDomainRecordsRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_api_pb_cluster_proto_rawDescGZIP(), []int{7}
|
return file_api_pb_cluster_proto_rawDescGZIP(), []int{8}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord {
|
func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord {
|
||||||
@@ -538,7 +596,7 @@ type CreateDomainRecordsResponse struct {
|
|||||||
func (x *CreateDomainRecordsResponse) Reset() {
|
func (x *CreateDomainRecordsResponse) Reset() {
|
||||||
*x = CreateDomainRecordsResponse{}
|
*x = CreateDomainRecordsResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_api_pb_cluster_proto_msgTypes[8]
|
mi := &file_api_pb_cluster_proto_msgTypes[9]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -551,7 +609,7 @@ func (x *CreateDomainRecordsResponse) String() string {
|
|||||||
func (*CreateDomainRecordsResponse) ProtoMessage() {}
|
func (*CreateDomainRecordsResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
|
func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_api_pb_cluster_proto_msgTypes[8]
|
mi := &file_api_pb_cluster_proto_msgTypes[9]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -564,7 +622,7 @@ func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use CreateDomainRecordsResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use CreateDomainRecordsResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*CreateDomainRecordsResponse) Descriptor() ([]byte, []int) {
|
func (*CreateDomainRecordsResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_api_pb_cluster_proto_rawDescGZIP(), []int{8}
|
return file_api_pb_cluster_proto_rawDescGZIP(), []int{9}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord {
|
func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord {
|
||||||
@@ -587,7 +645,7 @@ type DNSRecord struct {
|
|||||||
func (x *DNSRecord) Reset() {
|
func (x *DNSRecord) Reset() {
|
||||||
*x = DNSRecord{}
|
*x = DNSRecord{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_api_pb_cluster_proto_msgTypes[9]
|
mi := &file_api_pb_cluster_proto_msgTypes[10]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -600,7 +658,7 @@ func (x *DNSRecord) String() string {
|
|||||||
func (*DNSRecord) ProtoMessage() {}
|
func (*DNSRecord) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *DNSRecord) ProtoReflect() protoreflect.Message {
|
func (x *DNSRecord) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_api_pb_cluster_proto_msgTypes[9]
|
mi := &file_api_pb_cluster_proto_msgTypes[10]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -613,7 +671,7 @@ func (x *DNSRecord) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use DNSRecord.ProtoReflect.Descriptor instead.
|
// Deprecated: Use DNSRecord.ProtoReflect.Descriptor instead.
|
||||||
func (*DNSRecord) Descriptor() ([]byte, []int) {
|
func (*DNSRecord) Descriptor() ([]byte, []int) {
|
||||||
return file_api_pb_cluster_proto_rawDescGZIP(), []int{9}
|
return file_api_pb_cluster_proto_rawDescGZIP(), []int{10}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *DNSRecord) GetName() string {
|
func (x *DNSRecord) GetName() string {
|
||||||
@@ -676,63 +734,72 @@ var file_api_pb_cluster_proto_rawDesc = []byte{
|
|||||||
0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||||
0x73, 0x22, 0x26, 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
0x73, 0x22, 0x26, 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
||||||
0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
|
0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
|
||||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x1c, 0x0a, 0x06, 0x44, 0x6f, 0x6d,
|
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x4a, 0x0a, 0x06, 0x44, 0x6f, 0x6d,
|
||||||
0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x32, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x65, 0x72,
|
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x65, 0x72,
|
||||||
0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
0x76, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08, 0x72, 0x65, 0x73,
|
||||||
0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28,
|
0x65, 0x72, 0x76, 0x65, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x72, 0x65, 0x73,
|
||||||
0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x46, 0x0a, 0x1a, 0x43,
|
0x65, 0x72, 0x76, 0x65, 0x64, 0x22, 0x32, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65,
|
||||||
0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72,
|
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a,
|
||||||
0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63,
|
0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69,
|
0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x26, 0x0a, 0x10, 0x53, 0x65, 0x74,
|
||||||
0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f,
|
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a,
|
||||||
0x72, 0x64, 0x73, 0x22, 0x47, 0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d,
|
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d,
|
||||||
0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
0x65, 0x22, 0x46, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69,
|
||||||
0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20,
|
0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
||||||
0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63,
|
0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
|
||||||
0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x96, 0x01, 0x0a,
|
0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
|
||||||
0x09, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
|
0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47, 0x0a, 0x1b, 0x43, 0x72, 0x65,
|
||||||
0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d,
|
0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73,
|
||||||
0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x61,
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f,
|
||||||
0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x2e, 0x52, 0x65, 0x63,
|
0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a,
|
0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72,
|
||||||
0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76,
|
0x64, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
|
||||||
0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x2e, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54,
|
0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||||
0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49,
|
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01,
|
||||||
0x45, 0x44, 0x10, 0x00, 0x12, 0x05, 0x0a, 0x01, 0x41, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41,
|
0x28, 0x0e, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f,
|
||||||
0x41, 0x41, 0x41, 0x10, 0x02, 0x32, 0xca, 0x03, 0x0a, 0x07, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
0x72, 0x64, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74,
|
||||||
0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12,
|
0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20,
|
||||||
0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x2e, 0x0a, 0x0a, 0x52,
|
||||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64,
|
0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53,
|
||||||
0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x05, 0x0a, 0x01, 0x41, 0x10,
|
||||||
0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
|
0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02, 0x32, 0x86, 0x04, 0x0a, 0x07,
|
||||||
0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
|
0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61,
|
||||||
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c,
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d,
|
||||||
0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e,
|
||||||
0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63,
|
0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
|
||||||
0x68, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76,
|
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61,
|
||||||
0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||||
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19,
|
||||||
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||||
|
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x0d, 0x52, 0x65, 0x6d,
|
||||||
|
0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69,
|
||||||
|
0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
|
||||||
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
||||||
|
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x37, 0x0a,
|
||||||
|
0x0d, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x19,
|
||||||
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61,
|
||||||
|
0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
|
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x44, 0x6f, 0x6d,
|
||||||
|
0x61, 0x69, 0x6e, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x74, 0x44, 0x6f, 0x6d,
|
||||||
|
0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
|
||||||
|
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
|
||||||
|
0x74, 0x79, 0x12, 0x30, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12,
|
||||||
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
||||||
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x37, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x65, 0x72,
|
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f,
|
||||||
0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52,
|
0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44,
|
||||||
0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75,
|
0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
||||||
0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
|
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e,
|
||||||
0x12, 0x30, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e,
|
0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x58, 0x0a, 0x13, 0x43, 0x72,
|
||||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
|
||||||
0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61,
|
0x73, 0x12, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f,
|
||||||
0x69, 0x6e, 0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x6f, 0x6d,
|
0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||||
0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44,
|
||||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70,
|
0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70,
|
||||||
0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x58, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61,
|
0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
|
||||||
0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12,
|
0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e,
|
||||||
0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61,
|
0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
|
||||||
0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
0x6f, 0x74, 0x6f, 0x33,
|
||||||
0x1a, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d,
|
|
||||||
0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
|
||||||
0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
|
||||||
0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c,
|
|
||||||
0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
|
||||||
0x6f, 0x33,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -748,7 +815,7 @@ func file_api_pb_cluster_proto_rawDescGZIP() []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var file_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
|
var file_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
|
||||||
var file_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
|
var file_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
|
||||||
var file_api_pb_cluster_proto_goTypes = []any{
|
var file_api_pb_cluster_proto_goTypes = []any{
|
||||||
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
|
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
|
||||||
(DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType
|
(DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType
|
||||||
@@ -759,40 +826,43 @@ var file_api_pb_cluster_proto_goTypes = []any{
|
|||||||
(*RemoveMachineRequest)(nil), // 6: api.RemoveMachineRequest
|
(*RemoveMachineRequest)(nil), // 6: api.RemoveMachineRequest
|
||||||
(*Domain)(nil), // 7: api.Domain
|
(*Domain)(nil), // 7: api.Domain
|
||||||
(*ReserveDomainRequest)(nil), // 8: api.ReserveDomainRequest
|
(*ReserveDomainRequest)(nil), // 8: api.ReserveDomainRequest
|
||||||
(*CreateDomainRecordsRequest)(nil), // 9: api.CreateDomainRecordsRequest
|
(*SetDomainRequest)(nil), // 9: api.SetDomainRequest
|
||||||
(*CreateDomainRecordsResponse)(nil), // 10: api.CreateDomainRecordsResponse
|
(*CreateDomainRecordsRequest)(nil), // 10: api.CreateDomainRecordsRequest
|
||||||
(*DNSRecord)(nil), // 11: api.DNSRecord
|
(*CreateDomainRecordsResponse)(nil), // 11: api.CreateDomainRecordsResponse
|
||||||
(*NetworkConfig)(nil), // 12: api.NetworkConfig
|
(*DNSRecord)(nil), // 12: api.DNSRecord
|
||||||
(*IP)(nil), // 13: api.IP
|
(*NetworkConfig)(nil), // 13: api.NetworkConfig
|
||||||
(*MachineInfo)(nil), // 14: api.MachineInfo
|
(*IP)(nil), // 14: api.IP
|
||||||
(*emptypb.Empty)(nil), // 15: google.protobuf.Empty
|
(*MachineInfo)(nil), // 15: api.MachineInfo
|
||||||
|
(*emptypb.Empty)(nil), // 16: google.protobuf.Empty
|
||||||
}
|
}
|
||||||
var file_api_pb_cluster_proto_depIdxs = []int32{
|
var file_api_pb_cluster_proto_depIdxs = []int32{
|
||||||
12, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
|
13, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
|
||||||
13, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
|
14, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
|
||||||
14, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
|
15, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
|
||||||
14, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
|
15, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
|
||||||
0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
|
0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
|
||||||
4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember
|
4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember
|
||||||
11, // 6: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
|
12, // 6: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
|
||||||
11, // 7: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
|
12, // 7: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
|
||||||
1, // 8: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
|
1, // 8: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
|
||||||
2, // 9: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
|
2, // 9: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
|
||||||
15, // 10: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
|
16, // 10: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
|
||||||
6, // 11: api.Cluster.RemoveMachine:input_type -> api.RemoveMachineRequest
|
6, // 11: api.Cluster.RemoveMachine:input_type -> api.RemoveMachineRequest
|
||||||
8, // 12: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
|
8, // 12: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
|
||||||
15, // 13: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
|
9, // 13: api.Cluster.SetDomain:input_type -> api.SetDomainRequest
|
||||||
15, // 14: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
|
16, // 14: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
|
||||||
9, // 15: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
|
16, // 15: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
|
||||||
3, // 16: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
|
10, // 16: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
|
||||||
5, // 17: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
|
3, // 17: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
|
||||||
15, // 18: api.Cluster.RemoveMachine:output_type -> google.protobuf.Empty
|
5, // 18: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
|
||||||
7, // 19: api.Cluster.ReserveDomain:output_type -> api.Domain
|
16, // 19: api.Cluster.RemoveMachine:output_type -> google.protobuf.Empty
|
||||||
7, // 20: api.Cluster.GetDomain:output_type -> api.Domain
|
7, // 20: api.Cluster.ReserveDomain:output_type -> api.Domain
|
||||||
7, // 21: api.Cluster.ReleaseDomain:output_type -> api.Domain
|
16, // 21: api.Cluster.SetDomain:output_type -> google.protobuf.Empty
|
||||||
10, // 22: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
|
7, // 22: api.Cluster.GetDomain:output_type -> api.Domain
|
||||||
16, // [16:23] is the sub-list for method output_type
|
7, // 23: api.Cluster.ReleaseDomain:output_type -> api.Domain
|
||||||
9, // [9:16] is the sub-list for method input_type
|
11, // 24: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
|
||||||
|
17, // [17:25] is the sub-list for method output_type
|
||||||
|
9, // [9:17] is the sub-list for method input_type
|
||||||
9, // [9:9] is the sub-list for extension type_name
|
9, // [9:9] is the sub-list for extension type_name
|
||||||
9, // [9:9] is the sub-list for extension extendee
|
9, // [9:9] is the sub-list for extension extendee
|
||||||
0, // [0:9] is the sub-list for field type_name
|
0, // [0:9] is the sub-list for field type_name
|
||||||
@@ -891,7 +961,7 @@ func file_api_pb_cluster_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_api_pb_cluster_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
file_api_pb_cluster_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*CreateDomainRecordsRequest); i {
|
switch v := v.(*SetDomainRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -903,7 +973,7 @@ func file_api_pb_cluster_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_api_pb_cluster_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
file_api_pb_cluster_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*CreateDomainRecordsResponse); i {
|
switch v := v.(*CreateDomainRecordsRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -915,6 +985,18 @@ func file_api_pb_cluster_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_api_pb_cluster_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
file_api_pb_cluster_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*CreateDomainRecordsResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_api_pb_cluster_proto_msgTypes[10].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*DNSRecord); i {
|
switch v := v.(*DNSRecord); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -927,13 +1009,14 @@ func file_api_pb_cluster_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
file_api_pb_cluster_proto_msgTypes[5].OneofWrappers = []any{}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_api_pb_cluster_proto_rawDesc,
|
RawDescriptor: file_api_pb_cluster_proto_rawDesc,
|
||||||
NumEnums: 2,
|
NumEnums: 2,
|
||||||
NumMessages: 10,
|
NumMessages: 11,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ service Cluster {
|
|||||||
rpc RemoveMachine(RemoveMachineRequest) returns (google.protobuf.Empty);
|
rpc RemoveMachine(RemoveMachineRequest) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
rpc ReserveDomain(ReserveDomainRequest) returns (Domain);
|
rpc ReserveDomain(ReserveDomainRequest) returns (Domain);
|
||||||
|
rpc SetDomain(SetDomainRequest) returns (google.protobuf.Empty);
|
||||||
rpc GetDomain(google.protobuf.Empty) returns (Domain);
|
rpc GetDomain(google.protobuf.Empty) returns (Domain);
|
||||||
rpc ReleaseDomain(google.protobuf.Empty) returns (Domain);
|
rpc ReleaseDomain(google.protobuf.Empty) returns (Domain);
|
||||||
rpc CreateDomainRecords(CreateDomainRecordsRequest) returns (CreateDomainRecordsResponse);
|
rpc CreateDomainRecords(CreateDomainRecordsRequest) returns (CreateDomainRecordsResponse);
|
||||||
@@ -56,12 +57,20 @@ message RemoveMachineRequest {
|
|||||||
|
|
||||||
message Domain {
|
message Domain {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
|
// Whether the domain is reserved in Uncloud DNS. Older daemons (<0.21) omit this field
|
||||||
|
// and only support reserved domains, so clients should treat absence as true.
|
||||||
|
optional bool reserved = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ReserveDomainRequest {
|
message ReserveDomainRequest {
|
||||||
string endpoint = 1;
|
string endpoint = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message SetDomainRequest {
|
||||||
|
// An externally managed domain name. Empty clears a manually set domain.
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message CreateDomainRecordsRequest {
|
message CreateDomainRecordsRequest {
|
||||||
repeated DNSRecord records = 1;
|
repeated DNSRecord records = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const (
|
|||||||
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
|
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
|
||||||
Cluster_RemoveMachine_FullMethodName = "/api.Cluster/RemoveMachine"
|
Cluster_RemoveMachine_FullMethodName = "/api.Cluster/RemoveMachine"
|
||||||
Cluster_ReserveDomain_FullMethodName = "/api.Cluster/ReserveDomain"
|
Cluster_ReserveDomain_FullMethodName = "/api.Cluster/ReserveDomain"
|
||||||
|
Cluster_SetDomain_FullMethodName = "/api.Cluster/SetDomain"
|
||||||
Cluster_GetDomain_FullMethodName = "/api.Cluster/GetDomain"
|
Cluster_GetDomain_FullMethodName = "/api.Cluster/GetDomain"
|
||||||
Cluster_ReleaseDomain_FullMethodName = "/api.Cluster/ReleaseDomain"
|
Cluster_ReleaseDomain_FullMethodName = "/api.Cluster/ReleaseDomain"
|
||||||
Cluster_CreateDomainRecords_FullMethodName = "/api.Cluster/CreateDomainRecords"
|
Cluster_CreateDomainRecords_FullMethodName = "/api.Cluster/CreateDomainRecords"
|
||||||
@@ -37,6 +38,7 @@ type ClusterClient interface {
|
|||||||
ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error)
|
ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error)
|
||||||
RemoveMachine(ctx context.Context, in *RemoveMachineRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
RemoveMachine(ctx context.Context, in *RemoveMachineRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error)
|
ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error)
|
||||||
|
SetDomain(ctx context.Context, in *SetDomainRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
|
GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
|
||||||
ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
|
ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
|
||||||
CreateDomainRecords(ctx context.Context, in *CreateDomainRecordsRequest, opts ...grpc.CallOption) (*CreateDomainRecordsResponse, error)
|
CreateDomainRecords(ctx context.Context, in *CreateDomainRecordsRequest, opts ...grpc.CallOption) (*CreateDomainRecordsResponse, error)
|
||||||
@@ -90,6 +92,16 @@ func (c *clusterClient) ReserveDomain(ctx context.Context, in *ReserveDomainRequ
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *clusterClient) SetDomain(ctx context.Context, in *SetDomainRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(emptypb.Empty)
|
||||||
|
err := c.cc.Invoke(ctx, Cluster_SetDomain_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *clusterClient) GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) {
|
func (c *clusterClient) GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(Domain)
|
out := new(Domain)
|
||||||
@@ -128,6 +140,7 @@ type ClusterServer interface {
|
|||||||
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
|
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
|
||||||
RemoveMachine(context.Context, *RemoveMachineRequest) (*emptypb.Empty, error)
|
RemoveMachine(context.Context, *RemoveMachineRequest) (*emptypb.Empty, error)
|
||||||
ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error)
|
ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error)
|
||||||
|
SetDomain(context.Context, *SetDomainRequest) (*emptypb.Empty, error)
|
||||||
GetDomain(context.Context, *emptypb.Empty) (*Domain, error)
|
GetDomain(context.Context, *emptypb.Empty) (*Domain, error)
|
||||||
ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error)
|
ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error)
|
||||||
CreateDomainRecords(context.Context, *CreateDomainRecordsRequest) (*CreateDomainRecordsResponse, error)
|
CreateDomainRecords(context.Context, *CreateDomainRecordsRequest) (*CreateDomainRecordsResponse, error)
|
||||||
@@ -153,6 +166,9 @@ func (UnimplementedClusterServer) RemoveMachine(context.Context, *RemoveMachineR
|
|||||||
func (UnimplementedClusterServer) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) {
|
func (UnimplementedClusterServer) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ReserveDomain not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method ReserveDomain not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedClusterServer) SetDomain(context.Context, *SetDomainRequest) (*emptypb.Empty, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method SetDomain not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedClusterServer) GetDomain(context.Context, *emptypb.Empty) (*Domain, error) {
|
func (UnimplementedClusterServer) GetDomain(context.Context, *emptypb.Empty) (*Domain, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method GetDomain not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method GetDomain not implemented")
|
||||||
}
|
}
|
||||||
@@ -255,6 +271,24 @@ func _Cluster_ReserveDomain_Handler(srv interface{}, ctx context.Context, dec fu
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Cluster_SetDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(SetDomainRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ClusterServer).SetDomain(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Cluster_SetDomain_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ClusterServer).SetDomain(ctx, req.(*SetDomainRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Cluster_GetDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Cluster_GetDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(emptypb.Empty)
|
in := new(emptypb.Empty)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -332,6 +366,10 @@ var Cluster_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "ReserveDomain",
|
MethodName: "ReserveDomain",
|
||||||
Handler: _Cluster_ReserveDomain_Handler,
|
Handler: _Cluster_ReserveDomain_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "SetDomain",
|
||||||
|
Handler: _Cluster_SetDomain_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "GetDomain",
|
MethodName: "GetDomain",
|
||||||
Handler: _Cluster_GetDomain_Handler,
|
Handler: _Cluster_GetDomain_Handler,
|
||||||
|
|||||||
@@ -4,8 +4,16 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Err returns the upstream error, preserving its gRPC status code.
|
||||||
|
// It returns nil for nil metadata or an absent or successful status.
|
||||||
|
func (m *Metadata) Err() error {
|
||||||
|
return status.ErrorProto(m.GetStatus())
|
||||||
|
}
|
||||||
|
|
||||||
func NewIP(addr netip.Addr) *IP {
|
func NewIP(addr netip.Addr) *IP {
|
||||||
// MarshalBinary always returns a nil error.
|
// MarshalBinary always returns a nil error.
|
||||||
ip, _ := addr.MarshalBinary()
|
ip, _ := addr.MarshalBinary()
|
||||||
|
|||||||
+2
-1
@@ -87,7 +87,8 @@ type Metadata struct {
|
|||||||
MachineName string `protobuf:"bytes,5,opt,name=machine_name,json=machineName,proto3" json:"machine_name,omitempty"`
|
MachineName string `protobuf:"bytes,5,opt,name=machine_name,json=machineName,proto3" json:"machine_name,omitempty"`
|
||||||
// Address of the machine the response came from.
|
// Address of the machine the response came from.
|
||||||
MachineAddr string `protobuf:"bytes,1,opt,name=machine_addr,json=machineAddr,proto3" json:"machine_addr,omitempty"`
|
MachineAddr string `protobuf:"bytes,1,opt,name=machine_addr,json=machineAddr,proto3" json:"machine_addr,omitempty"`
|
||||||
// error is set if the request to upstream failed. The rest of the response is undefined.
|
// error is set if the request to upstream failed. The rest of the response apart from this metadata message
|
||||||
|
// is undefined.
|
||||||
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
|
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
|
||||||
// error as a gRPC Status message.
|
// error as a gRPC Status message.
|
||||||
Status *status.Status `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
|
Status *status.Status `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
|
||||||
|
|||||||
+2
-1
@@ -18,7 +18,8 @@ message Metadata {
|
|||||||
// Address of the machine the response came from.
|
// Address of the machine the response came from.
|
||||||
string machine_addr = 1;
|
string machine_addr = 1;
|
||||||
|
|
||||||
// error is set if the request to upstream failed. The rest of the response is undefined.
|
// error is set if the request to upstream failed. The rest of the response apart from this metadata message
|
||||||
|
// is undefined.
|
||||||
string error = 2;
|
string error = 2;
|
||||||
// error as a gRPC Status message.
|
// error as a gRPC Status message.
|
||||||
google.rpc.Status status = 3;
|
google.rpc.Status status = 3;
|
||||||
|
|||||||
+909
-295
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@ service Docker {
|
|||||||
// Docker auth credentials if necessary.
|
// Docker auth credentials if necessary.
|
||||||
rpc InspectRemoteImage(InspectRemoteImageRequest) returns (InspectRemoteImageResponse);
|
rpc InspectRemoteImage(InspectRemoteImageRequest) returns (InspectRemoteImageResponse);
|
||||||
rpc ListImages(ListImagesRequest) returns (ListImagesResponse);
|
rpc ListImages(ListImagesRequest) returns (ListImagesResponse);
|
||||||
|
rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse);
|
||||||
|
|
||||||
rpc CreateVolume(CreateVolumeRequest) returns (CreateVolumeResponse);
|
rpc CreateVolume(CreateVolumeRequest) returns (CreateVolumeResponse);
|
||||||
rpc ListVolumes(ListVolumesRequest) returns (ListVolumesResponse);
|
rpc ListVolumes(ListVolumesRequest) returns (ListVolumesResponse);
|
||||||
@@ -31,6 +32,8 @@ service Docker {
|
|||||||
|
|
||||||
rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse);
|
rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse);
|
||||||
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
|
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
|
||||||
|
// UpdateServiceContainer updates the requested properties of a service container in place without recreating it.
|
||||||
|
rpc UpdateServiceContainer(UpdateServiceContainerRequest) returns (google.protobuf.Empty);
|
||||||
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
|
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
|
||||||
rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
||||||
}
|
}
|
||||||
@@ -179,6 +182,40 @@ message ListImagesRequest {
|
|||||||
bytes options = 1;
|
bytes options = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message RemoveImageRequest {
|
||||||
|
string id = 1;
|
||||||
|
bool force = 2;
|
||||||
|
bool prune_children = 3;
|
||||||
|
repeated ImagePlatform platforms = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImagePlatform describes an OCI image platform.
|
||||||
|
message ImagePlatform {
|
||||||
|
string architecture = 1;
|
||||||
|
string os = 2;
|
||||||
|
string os_version = 3;
|
||||||
|
repeated string os_features = 4;
|
||||||
|
string variant = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveImageResponse {
|
||||||
|
// Must contain only one repeated messages field to allow broadcasting RemoveImage requests to multiple machines.
|
||||||
|
repeated MachineImageRemoval messages = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MachineImageRemoval {
|
||||||
|
Metadata metadata = 1;
|
||||||
|
repeated ImageRemoval results = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageRemoval reports an image deletion or an untagged image reference.
|
||||||
|
message ImageRemoval {
|
||||||
|
// The image ID of an image that was deleted.
|
||||||
|
string deleted = 1;
|
||||||
|
// The image reference of an image that was untagged.
|
||||||
|
string untagged = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message ListImagesResponse {
|
message ListImagesResponse {
|
||||||
// Must contain only one repeated messages field to allow broadcasting ListImages requests to multiple machines.
|
// Must contain only one repeated messages field to allow broadcasting ListImages requests to multiple machines.
|
||||||
repeated MachineImages messages = 1;
|
repeated MachineImages messages = 1;
|
||||||
@@ -246,6 +283,23 @@ message ServiceContainer {
|
|||||||
bytes service_spec = 2;
|
bytes service_spec = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message UpdateServiceContainerRequest {
|
||||||
|
string id = 1;
|
||||||
|
// Fields that are not set are left unchanged. CPU and memory limits can be changed but not removed.
|
||||||
|
// CPU limit in nanocores.
|
||||||
|
optional int64 cpu = 2;
|
||||||
|
// Memory limit in bytes.
|
||||||
|
optional int64 memory = 3;
|
||||||
|
// Memory reservation in bytes.
|
||||||
|
optional int64 memory_reservation = 4;
|
||||||
|
// Service labels that replace all current service labels.
|
||||||
|
Labels service_labels = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Labels {
|
||||||
|
map<string, string> labels = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message ListServiceContainersRequest {
|
message ListServiceContainersRequest {
|
||||||
string service_id = 1;
|
string service_id = 1;
|
||||||
// JSON serialised container.ListOptions.
|
// JSON serialised container.ListOptions.
|
||||||
|
|||||||
@@ -32,11 +32,13 @@ const (
|
|||||||
Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage"
|
Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage"
|
||||||
Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage"
|
Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage"
|
||||||
Docker_ListImages_FullMethodName = "/api.Docker/ListImages"
|
Docker_ListImages_FullMethodName = "/api.Docker/ListImages"
|
||||||
|
Docker_RemoveImage_FullMethodName = "/api.Docker/RemoveImage"
|
||||||
Docker_CreateVolume_FullMethodName = "/api.Docker/CreateVolume"
|
Docker_CreateVolume_FullMethodName = "/api.Docker/CreateVolume"
|
||||||
Docker_ListVolumes_FullMethodName = "/api.Docker/ListVolumes"
|
Docker_ListVolumes_FullMethodName = "/api.Docker/ListVolumes"
|
||||||
Docker_RemoveVolume_FullMethodName = "/api.Docker/RemoveVolume"
|
Docker_RemoveVolume_FullMethodName = "/api.Docker/RemoveVolume"
|
||||||
Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer"
|
Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer"
|
||||||
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
|
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
|
||||||
|
Docker_UpdateServiceContainer_FullMethodName = "/api.Docker/UpdateServiceContainer"
|
||||||
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
|
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
|
||||||
Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer"
|
Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer"
|
||||||
)
|
)
|
||||||
@@ -59,11 +61,14 @@ type DockerClient interface {
|
|||||||
// Docker auth credentials if necessary.
|
// Docker auth credentials if necessary.
|
||||||
InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error)
|
InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error)
|
||||||
ListImages(ctx context.Context, in *ListImagesRequest, opts ...grpc.CallOption) (*ListImagesResponse, error)
|
ListImages(ctx context.Context, in *ListImagesRequest, opts ...grpc.CallOption) (*ListImagesResponse, error)
|
||||||
|
RemoveImage(ctx context.Context, in *RemoveImageRequest, opts ...grpc.CallOption) (*RemoveImageResponse, error)
|
||||||
CreateVolume(ctx context.Context, in *CreateVolumeRequest, opts ...grpc.CallOption) (*CreateVolumeResponse, error)
|
CreateVolume(ctx context.Context, in *CreateVolumeRequest, opts ...grpc.CallOption) (*CreateVolumeResponse, error)
|
||||||
ListVolumes(ctx context.Context, in *ListVolumesRequest, opts ...grpc.CallOption) (*ListVolumesResponse, error)
|
ListVolumes(ctx context.Context, in *ListVolumesRequest, opts ...grpc.CallOption) (*ListVolumesResponse, error)
|
||||||
RemoveVolume(ctx context.Context, in *RemoveVolumeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
RemoveVolume(ctx context.Context, in *RemoveVolumeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
|
CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
|
||||||
InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error)
|
InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error)
|
||||||
|
// UpdateServiceContainer updates the requested properties of a service container in place without recreating it.
|
||||||
|
UpdateServiceContainer(ctx context.Context, in *UpdateServiceContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error)
|
ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error)
|
||||||
RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
}
|
}
|
||||||
@@ -217,6 +222,16 @@ func (c *dockerClient) ListImages(ctx context.Context, in *ListImagesRequest, op
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) RemoveImage(ctx context.Context, in *RemoveImageRequest, opts ...grpc.CallOption) (*RemoveImageResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(RemoveImageResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_RemoveImage_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *dockerClient) CreateVolume(ctx context.Context, in *CreateVolumeRequest, opts ...grpc.CallOption) (*CreateVolumeResponse, error) {
|
func (c *dockerClient) CreateVolume(ctx context.Context, in *CreateVolumeRequest, opts ...grpc.CallOption) (*CreateVolumeResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(CreateVolumeResponse)
|
out := new(CreateVolumeResponse)
|
||||||
@@ -267,6 +282,16 @@ func (c *dockerClient) InspectServiceContainer(ctx context.Context, in *InspectC
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) UpdateServiceContainer(ctx context.Context, in *UpdateServiceContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(emptypb.Empty)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_UpdateServiceContainer_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *dockerClient) ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error) {
|
func (c *dockerClient) ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(ListServiceContainersResponse)
|
out := new(ListServiceContainersResponse)
|
||||||
@@ -305,11 +330,14 @@ type DockerServer interface {
|
|||||||
// Docker auth credentials if necessary.
|
// Docker auth credentials if necessary.
|
||||||
InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error)
|
InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error)
|
||||||
ListImages(context.Context, *ListImagesRequest) (*ListImagesResponse, error)
|
ListImages(context.Context, *ListImagesRequest) (*ListImagesResponse, error)
|
||||||
|
RemoveImage(context.Context, *RemoveImageRequest) (*RemoveImageResponse, error)
|
||||||
CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error)
|
CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error)
|
||||||
ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error)
|
ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error)
|
||||||
RemoveVolume(context.Context, *RemoveVolumeRequest) (*emptypb.Empty, error)
|
RemoveVolume(context.Context, *RemoveVolumeRequest) (*emptypb.Empty, error)
|
||||||
CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error)
|
CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error)
|
||||||
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
|
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
|
||||||
|
// UpdateServiceContainer updates the requested properties of a service container in place without recreating it.
|
||||||
|
UpdateServiceContainer(context.Context, *UpdateServiceContainerRequest) (*emptypb.Empty, error)
|
||||||
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
|
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
|
||||||
RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
||||||
mustEmbedUnimplementedDockerServer()
|
mustEmbedUnimplementedDockerServer()
|
||||||
@@ -358,6 +386,9 @@ func (UnimplementedDockerServer) InspectRemoteImage(context.Context, *InspectRem
|
|||||||
func (UnimplementedDockerServer) ListImages(context.Context, *ListImagesRequest) (*ListImagesResponse, error) {
|
func (UnimplementedDockerServer) ListImages(context.Context, *ListImagesRequest) (*ListImagesResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ListImages not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method ListImages not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedDockerServer) RemoveImage(context.Context, *RemoveImageRequest) (*RemoveImageResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method RemoveImage not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedDockerServer) CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error) {
|
func (UnimplementedDockerServer) CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method CreateVolume not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method CreateVolume not implemented")
|
||||||
}
|
}
|
||||||
@@ -373,6 +404,9 @@ func (UnimplementedDockerServer) CreateServiceContainer(context.Context, *Create
|
|||||||
func (UnimplementedDockerServer) InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error) {
|
func (UnimplementedDockerServer) InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method InspectServiceContainer not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method InspectServiceContainer not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedDockerServer) UpdateServiceContainer(context.Context, *UpdateServiceContainerRequest) (*emptypb.Empty, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method UpdateServiceContainer not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedDockerServer) ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error) {
|
func (UnimplementedDockerServer) ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ListServiceContainers not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method ListServiceContainers not implemented")
|
||||||
}
|
}
|
||||||
@@ -591,6 +625,24 @@ func _Docker_ListImages_Handler(srv interface{}, ctx context.Context, dec func(i
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Docker_RemoveImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RemoveImageRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).RemoveImage(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_RemoveImage_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).RemoveImage(ctx, req.(*RemoveImageRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Docker_CreateVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Docker_CreateVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(CreateVolumeRequest)
|
in := new(CreateVolumeRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -681,6 +733,24 @@ func _Docker_InspectServiceContainer_Handler(srv interface{}, ctx context.Contex
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Docker_UpdateServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UpdateServiceContainerRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).UpdateServiceContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_UpdateServiceContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).UpdateServiceContainer(ctx, req.(*UpdateServiceContainerRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Docker_ListServiceContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Docker_ListServiceContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(ListServiceContainersRequest)
|
in := new(ListServiceContainersRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -760,6 +830,10 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "ListImages",
|
MethodName: "ListImages",
|
||||||
Handler: _Docker_ListImages_Handler,
|
Handler: _Docker_ListImages_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RemoveImage",
|
||||||
|
Handler: _Docker_RemoveImage_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "CreateVolume",
|
MethodName: "CreateVolume",
|
||||||
Handler: _Docker_CreateVolume_Handler,
|
Handler: _Docker_CreateVolume_Handler,
|
||||||
@@ -780,6 +854,10 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "InspectServiceContainer",
|
MethodName: "InspectServiceContainer",
|
||||||
Handler: _Docker_InspectServiceContainer_Handler,
|
Handler: _Docker_InspectServiceContainer_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "UpdateServiceContainer",
|
||||||
|
Handler: _Docker_UpdateServiceContainer_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "ListServiceContainers",
|
MethodName: "ListServiceContainers",
|
||||||
Handler: _Docker_ListServiceContainers_Handler,
|
Handler: _Docker_ListServiceContainers_Handler,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ service Machine {
|
|||||||
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
|
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
|
||||||
rpc InspectMachine(google.protobuf.Empty) returns (InspectMachineResponse);
|
rpc InspectMachine(google.protobuf.Empty) returns (InspectMachineResponse);
|
||||||
// WaitForStoreVersion waits until the cluster store on this machine has reached each requested actor version, with
|
// WaitForStoreVersion waits until the cluster store on this machine has reached each requested actor version, with
|
||||||
// no known missing or pending transactions through those versions.
|
// no known missing or pending transactions through those versions from active members.
|
||||||
// Corrosion may satisfy a version by applying its surviving changes or by marking it complete because its changes
|
// Corrosion may satisfy a version by applying its surviving changes or by marking it complete because its changes
|
||||||
// have been superseded.
|
// have been superseded.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ type MachineClient interface {
|
|||||||
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
|
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
|
||||||
InspectMachine(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectMachineResponse, error)
|
InspectMachine(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectMachineResponse, error)
|
||||||
// WaitForStoreVersion waits until the cluster store on this machine has reached each requested actor version, with
|
// WaitForStoreVersion waits until the cluster store on this machine has reached each requested actor version, with
|
||||||
// no known missing or pending transactions through those versions.
|
// no known missing or pending transactions through those versions from active members.
|
||||||
// Corrosion may satisfy a version by applying its surviving changes or by marking it complete because its changes
|
// Corrosion may satisfy a version by applying its surviving changes or by marking it complete because its changes
|
||||||
// have been superseded.
|
// have been superseded.
|
||||||
//
|
//
|
||||||
@@ -224,7 +224,7 @@ type MachineServer interface {
|
|||||||
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
|
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
|
||||||
InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error)
|
InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error)
|
||||||
// WaitForStoreVersion waits until the cluster store on this machine has reached each requested actor version, with
|
// WaitForStoreVersion waits until the cluster store on this machine has reached each requested actor version, with
|
||||||
// no known missing or pending transactions through those versions.
|
// no known missing or pending transactions through those versions from active members.
|
||||||
// Corrosion may satisfy a version by applying its surviving changes or by marking it complete because its changes
|
// Corrosion may satisfy a version by applying its surviving changes or by marking it complete because its changes
|
||||||
// have been superseded.
|
// have been superseded.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
package caddystorage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"slices"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/pkg/client"
|
|
||||||
"github.com/psviderski/uncloud/pkg/distlock"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
lockPrefix = "caddy_storage:"
|
|
||||||
storeReplicationTimeout = 10 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
// Lock acquires an automatically renewed distributed lock and waits for the local store to catch up with versions
|
|
||||||
// observed on responding machines. Writers using the same lock can then read locally. Unavailable machines may have
|
|
||||||
// writes that this wait does not cover, and reads outside a lock remain eventually consistent.
|
|
||||||
func (s *Storage) Lock(ctx context.Context, name string) (err error) {
|
|
||||||
if name == "" {
|
|
||||||
return errors.New("lock name is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count the call first. If Cleanup has already observed zero, Caddy has cancelled s.ctx and the check below rejects
|
|
||||||
// this call before it uses the client.
|
|
||||||
s.lockOps.Add(1)
|
|
||||||
// A failed Lock owns its lifecycle through any lease rollback. A successful Lock transfers that responsibility to
|
|
||||||
// Unlock, which keeps the client open until its release attempt finishes.
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
s.lockOps.Add(-1)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if s.ctx.Err() != nil {
|
|
||||||
return errors.New("storage is closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop acquisition retries when Caddy unloads the module (cancels s.ctx), even if the caller's context
|
|
||||||
// is still active.
|
|
||||||
ctx, cancel := context.WithCancelCause(ctx)
|
|
||||||
defer cancel(nil)
|
|
||||||
stopOnCleanup := context.AfterFunc(s.ctx, func() {
|
|
||||||
cancel(context.Cause(s.ctx))
|
|
||||||
})
|
|
||||||
defer stopOnCleanup()
|
|
||||||
|
|
||||||
log := s.log.With("lock", name)
|
|
||||||
started := time.Now()
|
|
||||||
log.Debug("acquiring lock", "ttl", time.Duration(s.LockTTL))
|
|
||||||
lease, err := s.locker.Acquire(ctx, lockPrefix+name)
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("failed to acquire lock", "duration", time.Since(started), "error", err)
|
|
||||||
return fmt.Errorf("acquire lock '%s': %w", name, err)
|
|
||||||
}
|
|
||||||
log.Debug("lock lease acquired", "duration", time.Since(started))
|
|
||||||
// Release the lease if the lock acquisition fails after this point.
|
|
||||||
// Unlock will take care of releasing the lease on success.
|
|
||||||
defer func() {
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if releaseErr := s.releaseLock(ctx, name, lease, "cancelled acquisition"); releaseErr != nil {
|
|
||||||
err = errors.Join(err, releaseErr)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Catch up with the latest store versions observed on responding machines to increase the chance of reading
|
|
||||||
// the latest writes on them locally.
|
|
||||||
version, machines, err := s.clusterStoreVersion(ctx, log)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
waitStarted := time.Now()
|
|
||||||
log.Debug("waiting for local store replication", "machine_names", machines, "store_version", version)
|
|
||||||
waitCtx, cancelWait := context.WithTimeout(ctx, storeReplicationTimeout)
|
|
||||||
err = s.client.WaitForStoreVersion(waitCtx, version)
|
|
||||||
cancelWait()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("wait for local store replication: %w", err)
|
|
||||||
}
|
|
||||||
log.Debug("local store replication complete", "duration", time.Since(waitStarted))
|
|
||||||
|
|
||||||
s.locksMu.Lock()
|
|
||||||
defer s.locksMu.Unlock()
|
|
||||||
if s.ctx.Err() != nil {
|
|
||||||
return errors.New("storage is closed")
|
|
||||||
}
|
|
||||||
if lost := context.Cause(lease.Context()); lost != nil {
|
|
||||||
return lost
|
|
||||||
}
|
|
||||||
if _, exists := s.locks[name]; exists {
|
|
||||||
return errors.New("lock is already tracked by this storage instance")
|
|
||||||
}
|
|
||||||
s.locks[name] = lease
|
|
||||||
|
|
||||||
log.Debug("lock acquired", "duration", time.Since(started))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// clusterStoreVersion returns the per-actor maximum store versions from responding machines and their names.
|
|
||||||
func (s *Storage) clusterStoreVersion(ctx context.Context, log *slog.Logger) (map[string]uint64, []string, error) {
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, distlock.DefaultMaxNodeCallTimeout)
|
|
||||||
defer cancel()
|
|
||||||
resp, err := s.client.MachineClient.InspectMachine(client.ProxyMachinesContext(ctx, nil), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("inspect machines for store versions: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
maxVersion := make(map[string]uint64)
|
|
||||||
machines := make([]string, 0, len(resp.Machines))
|
|
||||||
for _, m := range resp.Machines {
|
|
||||||
if m.Metadata.Error != "" {
|
|
||||||
log.Warn("skipping machine when collecting store versions",
|
|
||||||
"id", m.Metadata.MachineId, "name", m.Metadata.MachineName, "error", m.Metadata.Error)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
machines = append(machines, m.Metadata.MachineName)
|
|
||||||
for actor, v := range m.StoreVersion {
|
|
||||||
maxVersion[actor] = max(maxVersion[actor], v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
slices.Sort(machines)
|
|
||||||
return maxVersion, machines, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlock releases a previously acquired distributed lock.
|
|
||||||
func (s *Storage) Unlock(ctx context.Context, name string) error {
|
|
||||||
s.locksMu.Lock()
|
|
||||||
lease, exists := s.locks[name]
|
|
||||||
if exists {
|
|
||||||
delete(s.locks, name)
|
|
||||||
}
|
|
||||||
s.locksMu.Unlock()
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("lock '%s' is not held by this storage instance", name)
|
|
||||||
}
|
|
||||||
defer s.lockOps.Add(-1)
|
|
||||||
|
|
||||||
// Release stops renewal even on error. Any nodes that cannot be reached will let the lease expire.
|
|
||||||
if err := s.releaseLock(ctx, name, lease, "unlock"); err != nil {
|
|
||||||
return fmt.Errorf("release lock '%s': %w", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// releaseLock releases the lease with a context that ignores cancellation and logs the release attempt.
|
|
||||||
func (s *Storage) releaseLock(ctx context.Context, name string, lease *distlock.Lease, reason string) error {
|
|
||||||
// Unlock and rollback must attempt node cleanup even if the caller or module has already been cancelled.
|
|
||||||
ctx = context.WithoutCancel(ctx)
|
|
||||||
|
|
||||||
log := s.log.With("lock", name, "reason", reason)
|
|
||||||
started := time.Now()
|
|
||||||
log.Debug("releasing lock")
|
|
||||||
if err := lease.Release(ctx); err != nil {
|
|
||||||
log.Debug("failed to release lock", "duration", time.Since(started), "error", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Debug("lock released", "duration", time.Since(started))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
// Package caddystorage provides Caddy storage backed by an Uncloud cluster.
|
|
||||||
package caddystorage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
||||||
"github.com/caddyserver/certmagic"
|
|
||||||
"github.com/psviderski/uncloud/pkg/client"
|
|
||||||
"github.com/psviderski/uncloud/pkg/client/connector"
|
|
||||||
"github.com/psviderski/uncloud/pkg/distlock"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ModuleID is the Caddy module ID for Uncloud storage.
|
|
||||||
ModuleID = "caddy.storage.uncloud"
|
|
||||||
// DefaultSocketPath is the default path to the Uncloud API socket.
|
|
||||||
DefaultSocketPath = "/run/uncloud/api/uncloud.sock"
|
|
||||||
// DefaultLockTTL is the default duration of a distributed lock lease.
|
|
||||||
DefaultLockTTL = 20 * time.Second
|
|
||||||
// lockCleanupTimeout bounds how long an unloaded module waits for active lock operations when cleaning up.
|
|
||||||
lockCleanupTimeout = 5 * time.Minute
|
|
||||||
)
|
|
||||||
|
|
||||||
//nolint:gochecknoinits // Caddy modules must register during package initialisation.
|
|
||||||
func init() {
|
|
||||||
caddy.RegisterModule(new(Storage))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Storage implements a Caddy storage backend that uses an Uncloud cluster to store assets such as TLS certificates.
|
|
||||||
type Storage struct {
|
|
||||||
// Socket is the path to the Uncloud API socket.
|
|
||||||
// Defaults to /run/uncloud/api/uncloud.sock when not set.
|
|
||||||
Socket string `json:"socket,omitempty"`
|
|
||||||
// LockTTL is the duration of a distributed lock after which it expires if not renewed. Locks renew automatically
|
|
||||||
// until unlocked. If an instance crashes or cannot renew, expiry allows another instance to acquire the stale lock.
|
|
||||||
// Longer durations tolerate longer interruptions but delay recovery after a crash. Normal unlocks release the lock
|
|
||||||
// immediately.
|
|
||||||
// Defaults to 20 seconds when not set.
|
|
||||||
LockTTL caddy.Duration `json:"lock_ttl,omitempty"`
|
|
||||||
|
|
||||||
client *client.Client
|
|
||||||
locker *distlock.Locker
|
|
||||||
log *slog.Logger
|
|
||||||
|
|
||||||
// ctx is the module context from Provision. Caddy cancels it when it unloads the module and calls Cleanup.
|
|
||||||
ctx context.Context
|
|
||||||
|
|
||||||
// locksMu protects locks.
|
|
||||||
locksMu sync.Mutex
|
|
||||||
// locks maps successfully acquired lock names to held leases.
|
|
||||||
locks map[string]*distlock.Lease
|
|
||||||
// lockOps counts Lock calls until they fail or their locks are released.
|
|
||||||
lockOps atomic.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// CaddyModule returns the Caddy module information.
|
|
||||||
func (*Storage) CaddyModule() caddy.ModuleInfo {
|
|
||||||
return caddy.ModuleInfo{
|
|
||||||
ID: ModuleID,
|
|
||||||
New: func() caddy.Module { return new(Storage) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provision connects the storage to the local Uncloud API and initialises the distributed locker.
|
|
||||||
func (s *Storage) Provision(ctx caddy.Context) error {
|
|
||||||
s.ctx = ctx
|
|
||||||
s.log = ctx.Slogger()
|
|
||||||
|
|
||||||
if s.Socket == "" {
|
|
||||||
s.Socket = DefaultSocketPath
|
|
||||||
}
|
|
||||||
if s.LockTTL == 0 {
|
|
||||||
s.LockTTL = caddy.Duration(DefaultLockTTL)
|
|
||||||
}
|
|
||||||
if s.LockTTL < 0 {
|
|
||||||
return errors.New("lock_ttl must be positive")
|
|
||||||
}
|
|
||||||
|
|
||||||
cli, err := client.New(ctx, connector.NewUnixConnector(s.Socket))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("connect to Uncloud API: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
locker, err := cli.NewLocker(distlock.Config{
|
|
||||||
LeaseDuration: time.Duration(s.LockTTL),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
_ = cli.Close()
|
|
||||||
return fmt.Errorf("create distributed locker: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.client = cli
|
|
||||||
s.locker = locker
|
|
||||||
s.locks = make(map[string]*distlock.Lease)
|
|
||||||
|
|
||||||
s.log.Info("module provisioned", "socket", s.Socket, "lock_ttl", time.Duration(s.LockTTL))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup keeps the Uncloud API connection open so acquired lock leases can renew until Caddy releases them. It closes
|
|
||||||
// the connection after all lock operations finish or the cleanup timeout expires.
|
|
||||||
func (s *Storage) Cleanup() error {
|
|
||||||
if s.client == nil {
|
|
||||||
// Provision failed before opening the storage.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
started := time.Now()
|
|
||||||
s.log.Debug("cleaning up module", "locks", s.lockOps.Load())
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
timer := time.NewTimer(lockCleanupTimeout)
|
|
||||||
defer timer.Stop()
|
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
timedOut := false
|
|
||||||
for !timedOut && s.lockOps.Load() > 0 {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
case <-timer.C:
|
|
||||||
timedOut = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if remaining := s.lockOps.Load(); timedOut && remaining > 0 {
|
|
||||||
s.log.Warn("timed out waiting for active locks to be unlocked",
|
|
||||||
"locks", remaining, "timeout", lockCleanupTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.client.Close(); err != nil {
|
|
||||||
s.log.Warn("failed to clean up module", "duration", time.Since(started), "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.log.Debug("module cleanup complete", "duration", time.Since(started))
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CertMagicStorage returns the provisioned CertMagic storage implementation.
|
|
||||||
func (s *Storage) CertMagicStorage() (certmagic.Storage, error) {
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalCaddyfile configures Uncloud storage from the Caddyfile global storage block.
|
|
||||||
//
|
|
||||||
// {
|
|
||||||
// storage uncloud {
|
|
||||||
// socket /run/uncloud/api/uncloud.sock
|
|
||||||
// lock_ttl 20s
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
func (s *Storage) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|
||||||
d.Next() // Skip the module name 'uncloud'.
|
|
||||||
// Reject inline arguments. NextArg leaves an opening brace for NextBlock.
|
|
||||||
if d.NextArg() {
|
|
||||||
return d.ArgErr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the optional options block, skipping its surrounding braces.
|
|
||||||
for d.NextBlock(0) {
|
|
||||||
switch d.Val() {
|
|
||||||
case "socket":
|
|
||||||
// Require a socket path on the same line as 'socket' option.
|
|
||||||
if !d.NextArg() {
|
|
||||||
return d.ArgErr()
|
|
||||||
}
|
|
||||||
s.Socket = d.Val()
|
|
||||||
// Reject extra arguments after the socket path.
|
|
||||||
if d.NextArg() {
|
|
||||||
return d.ArgErr()
|
|
||||||
}
|
|
||||||
case "lock_ttl":
|
|
||||||
if !d.NextArg() {
|
|
||||||
return d.ArgErr()
|
|
||||||
}
|
|
||||||
ttl, err := caddy.ParseDuration(d.Val())
|
|
||||||
if err != nil {
|
|
||||||
return d.Errf("invalid lock_ttl '%s': %w", d.Val(), err)
|
|
||||||
}
|
|
||||||
if ttl <= 0 {
|
|
||||||
return d.Err("lock_ttl must be positive")
|
|
||||||
}
|
|
||||||
if d.NextArg() {
|
|
||||||
return d.ArgErr()
|
|
||||||
}
|
|
||||||
s.LockTTL = caddy.Duration(ttl)
|
|
||||||
default:
|
|
||||||
return d.Errf("unknown uncloud storage option: '%s'", d.Val())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ caddy.Module = (*Storage)(nil)
|
|
||||||
_ caddy.Provisioner = (*Storage)(nil)
|
|
||||||
_ caddy.CleanerUpper = (*Storage)(nil)
|
|
||||||
_ caddy.StorageConverter = (*Storage)(nil)
|
|
||||||
_ caddyfile.Unmarshaler = (*Storage)(nil)
|
|
||||||
_ certmagic.Storage = (*Storage)(nil)
|
|
||||||
)
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package caddystorage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
|
||||||
"github.com/psviderski/uncloud/api/pb"
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Store writes a value to the cluster store.
|
|
||||||
func (s *Storage) Store(ctx context.Context, key string, value []byte) error {
|
|
||||||
if _, err := s.client.CaddyStorage.Store(ctx, &pb.StoreCaddyStorageRequest{Key: key, Value: value}); err != nil {
|
|
||||||
return storageError("store", key, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load reads a value from the cluster store.
|
|
||||||
func (s *Storage) Load(ctx context.Context, key string) ([]byte, error) {
|
|
||||||
resp, err := s.client.CaddyStorage.Load(ctx, &pb.LoadCaddyStorageRequest{Key: key})
|
|
||||||
if err != nil {
|
|
||||||
return nil, storageError("load", key, err)
|
|
||||||
}
|
|
||||||
return resp.Value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes a key and its descendants from the cluster store.
|
|
||||||
func (s *Storage) Delete(ctx context.Context, key string) error {
|
|
||||||
if _, err := s.client.CaddyStorage.Delete(ctx, &pb.DeleteCaddyStorageRequest{Key: key}); err != nil {
|
|
||||||
return storageError("delete", key, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists reports whether a key exists in the cluster store.
|
|
||||||
func (s *Storage) Exists(ctx context.Context, key string) bool {
|
|
||||||
_, err := s.Stat(ctx, key)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// List returns keys under prefix from the cluster store.
|
|
||||||
func (s *Storage) List(ctx context.Context, prefix string, recursive bool) ([]string, error) {
|
|
||||||
resp, err := s.client.CaddyStorage.List(ctx, &pb.ListCaddyStorageRequest{
|
|
||||||
Prefix: prefix,
|
|
||||||
Recursive: recursive,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, storageError("list", prefix, err)
|
|
||||||
}
|
|
||||||
return resp.Keys, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stat returns information about a key in the cluster store.
|
|
||||||
func (s *Storage) Stat(ctx context.Context, key string) (certmagic.KeyInfo, error) {
|
|
||||||
resp, err := s.client.CaddyStorage.Stat(ctx, &pb.StatCaddyStorageRequest{Key: key})
|
|
||||||
if err != nil {
|
|
||||||
return certmagic.KeyInfo{}, storageError("stat", key, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
info := certmagic.KeyInfo{
|
|
||||||
Key: resp.Key,
|
|
||||||
Size: resp.Size,
|
|
||||||
IsTerminal: resp.IsTerminal,
|
|
||||||
}
|
|
||||||
if resp.UpdatedAt != nil {
|
|
||||||
if err := resp.UpdatedAt.CheckValid(); err != nil {
|
|
||||||
return certmagic.KeyInfo{}, storageError("stat", key, fmt.Errorf("invalid updated_at timestamp: %w", err))
|
|
||||||
}
|
|
||||||
info.Modified = resp.UpdatedAt.AsTime()
|
|
||||||
}
|
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func storageError(operation, key string, err error) error {
|
|
||||||
if status.Code(err) == codes.NotFound {
|
|
||||||
err = fs.ErrNotExist
|
|
||||||
}
|
|
||||||
return fmt.Errorf("uncloud storage: %s key %q: %w", operation, key, err)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/completion"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
|
"github.com/psviderski/uncloud/internal/version"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listOptions struct {
|
||||||
|
machine string
|
||||||
|
output string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListCommand() *cobra.Command {
|
||||||
|
opts := listOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ls",
|
||||||
|
Aliases: []string{"list"},
|
||||||
|
Short: "List certificates in cluster storage for Caddy.",
|
||||||
|
Long: `List certificates stored in the Uncloud cluster storage for Caddy.
|
||||||
|
|
||||||
|
Caddy uses cluster storage by default. Certificates don't appear here if Caddy uses local storage,
|
||||||
|
for example, if it runs with a custom global config without 'storage uncloud' or was deployed with
|
||||||
|
uc v0.21.0 or earlier and not upgraded.
|
||||||
|
|
||||||
|
This inventory does not check whether Caddy currently serves or trusts a certificate.
|
||||||
|
|
||||||
|
See ` + version.DocsURL + `/concepts/ingress/cluster-storage-for-caddy for details.`,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return list(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
||||||
|
"Name or ID of the machine to read certificate storage from. (default is connected machine)")
|
||||||
|
cmd.Flags().StringVarP(&opts.output, "output", "o", "",
|
||||||
|
"Output format: 'json' or empty for a human-readable table.")
|
||||||
|
completion.MachinesFlag(cmd)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
||||||
|
if opts.output != "" && opts.output != "json" {
|
||||||
|
return fmt.Errorf("unsupported output format '%s' (supported: json)", opts.output)
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
certs, listErr := clusterClient.Caddy.ListCertificates(ctx,
|
||||||
|
client.CaddyListCertificatesOptions{Machine: opts.machine})
|
||||||
|
if len(certs) > 0 || listErr == nil {
|
||||||
|
if err = printCertificates(os.Stdout, certs, opts.output); err != nil {
|
||||||
|
return fmt.Errorf("print Caddy certificates: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if listErr != nil {
|
||||||
|
if len(certs) > 0 {
|
||||||
|
return fmt.Errorf("certificate list is partial: %w", listErr)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("list Caddy certificates: %w", listErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printCertificates(out io.Writer, certs []api.IssuedCertificate, output string) error {
|
||||||
|
type certificate struct {
|
||||||
|
api.IssuedCertificate
|
||||||
|
Fingerprint string
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]certificate, 0, len(certs))
|
||||||
|
for _, cert := range certs {
|
||||||
|
fingerprint := cert.Fingerprint()
|
||||||
|
items = append(items, certificate{
|
||||||
|
IssuedCertificate: cert,
|
||||||
|
Fingerprint: hex.EncodeToString(fingerprint[:]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
slices.SortFunc(items, func(a, b certificate) int {
|
||||||
|
if cmp := strings.Compare(a.SAN, b.SAN); cmp != 0 {
|
||||||
|
return cmp
|
||||||
|
}
|
||||||
|
if cmp := a.Chain[0].NotAfter.Compare(b.Chain[0].NotAfter); cmp != 0 {
|
||||||
|
return cmp
|
||||||
|
}
|
||||||
|
return strings.Compare(a.Fingerprint, b.Fingerprint)
|
||||||
|
})
|
||||||
|
|
||||||
|
if output == "json" {
|
||||||
|
encoder := json.NewEncoder(out)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
if err := encoder.Encode(items); err != nil {
|
||||||
|
return fmt.Errorf("marshal certificates: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(items) == 0 {
|
||||||
|
_, err := fmt.Fprintln(out, "No Caddy certificates found in cluster storage.")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t := tui.NewTable()
|
||||||
|
t.Headers("ID", "NAME", "ISSUER", "EXPIRES")
|
||||||
|
for _, item := range items {
|
||||||
|
t.Row(
|
||||||
|
item.Fingerprint[:12],
|
||||||
|
item.SAN,
|
||||||
|
formatIssuer(item.IssuerData),
|
||||||
|
formatExpiry(item.Chain[0].NotAfter),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_, err := lipgloss.Fprintln(out, t)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatIssuer(data api.CertificateIssuerData) string {
|
||||||
|
if data.ACME == nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch data.ACME.CA {
|
||||||
|
case certmagic.LetsEncryptProductionCA:
|
||||||
|
return "Let's Encrypt"
|
||||||
|
case certmagic.LetsEncryptStagingCA:
|
||||||
|
return "Let's Encrypt (staging)"
|
||||||
|
case certmagic.ZeroSSLProductionCA:
|
||||||
|
return "ZeroSSL"
|
||||||
|
case certmagic.GoogleTrustProductionCA:
|
||||||
|
return "Google Trust Services"
|
||||||
|
case certmagic.GoogleTrustStagingCA:
|
||||||
|
return "Google Trust Services (staging)"
|
||||||
|
default:
|
||||||
|
return data.ACME.CA
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatExpiry(expires time.Time) string {
|
||||||
|
delta := expires.Sub(time.Now())
|
||||||
|
if delta <= 0 {
|
||||||
|
return fmt.Sprintf("%s (expired %s ago)", expires.UTC().Format(time.DateOnly),
|
||||||
|
strings.ToLower(units.HumanDuration(-delta)))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s (%s)", expires.UTC().Format(time.DateOnly),
|
||||||
|
strings.ToLower(units.HumanDuration(delta)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPrintCertificates(t *testing.T) {
|
||||||
|
now := time.Now().UTC().Truncate(time.Hour)
|
||||||
|
certs := []api.IssuedCertificate{
|
||||||
|
testIssuedCertificate("z.example.com", "z", now.Add(48*time.Hour), certmagic.LetsEncryptProductionCA),
|
||||||
|
testIssuedCertificate("a.example.com", "old", now.Add(-48*time.Hour), certmagic.LetsEncryptStagingCA),
|
||||||
|
testIssuedCertificate("a.example.com", "new", now.Add(72*time.Hour), "https://ca.example/directory"),
|
||||||
|
testIssuedCertificate("unknown.example.com", "unknown", now.Add(96*time.Hour), ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
var table bytes.Buffer
|
||||||
|
require.NoError(t, printCertificates(&table, certs, ""))
|
||||||
|
output := table.String()
|
||||||
|
assert.Contains(t, output, "ID")
|
||||||
|
assert.Contains(t, output, "NAME")
|
||||||
|
assert.Contains(t, output, "ISSUER")
|
||||||
|
assert.Contains(t, output, "EXPIRES")
|
||||||
|
assert.Contains(t, output, now.Add(-48*time.Hour).Format(time.DateOnly)+" (expired ")
|
||||||
|
assert.Contains(t, output, now.Add(48*time.Hour).Format(time.DateOnly)+" (")
|
||||||
|
assert.Contains(t, output, "Let's Encrypt (staging)")
|
||||||
|
assert.Contains(t, output, "https://ca.example/directory")
|
||||||
|
assert.Less(t, strings.Index(output, now.Add(-48*time.Hour).Format(time.DateOnly)),
|
||||||
|
strings.Index(output, now.Add(72*time.Hour).Format(time.DateOnly)))
|
||||||
|
assert.Less(t, strings.Index(output, "a.example.com"), strings.Index(output, "z.example.com"))
|
||||||
|
assert.Contains(t, output, testFingerprint("old")[:12])
|
||||||
|
|
||||||
|
var encoded bytes.Buffer
|
||||||
|
require.NoError(t, printCertificates(&encoded, certs, "json"))
|
||||||
|
var items []struct {
|
||||||
|
api.IssuedCertificate
|
||||||
|
Fingerprint string
|
||||||
|
}
|
||||||
|
require.NoError(t, json.Unmarshal(encoded.Bytes(), &items))
|
||||||
|
require.Len(t, items, 4)
|
||||||
|
assert.Equal(t, testFingerprint("old"), items[0].Fingerprint)
|
||||||
|
assert.Equal(t, "a.example.com", items[0].SAN)
|
||||||
|
require.Len(t, items[0].Chain, 1)
|
||||||
|
assert.Equal(t, []byte("old"), items[0].Chain[0].Raw)
|
||||||
|
assert.Equal(t, now.Add(-48*time.Hour), items[0].Chain[0].NotAfter)
|
||||||
|
require.NotNil(t, items[0].IssuerData.ACME)
|
||||||
|
assert.Equal(t, certmagic.LetsEncryptStagingCA, items[0].IssuerData.ACME.CA)
|
||||||
|
assert.Equal(t, testFingerprint("new"), items[1].Fingerprint)
|
||||||
|
require.NotNil(t, items[1].IssuerData.ACME)
|
||||||
|
assert.Equal(t, "https://ca.example/directory", items[1].IssuerData.ACME.CA)
|
||||||
|
assert.Nil(t, items[2].IssuerData.ACME)
|
||||||
|
assert.Equal(t, "z.example.com", items[3].SAN)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintCertificates_Empty(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
output string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{output: "", want: "No Caddy certificates found in cluster storage.\n"},
|
||||||
|
{output: "json", want: "[]\n"},
|
||||||
|
} {
|
||||||
|
var out bytes.Buffer
|
||||||
|
require.NoError(t, printCertificates(&out, nil, tt.output))
|
||||||
|
assert.Equal(t, tt.want, out.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatIssuer(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
ca string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "production", ca: certmagic.LetsEncryptProductionCA, want: "Let's Encrypt"},
|
||||||
|
{name: "staging", ca: certmagic.LetsEncryptStagingCA, want: "Let's Encrypt (staging)"},
|
||||||
|
{name: "ZeroSSL", ca: certmagic.ZeroSSLProductionCA, want: "ZeroSSL"},
|
||||||
|
{name: "Google Trust Services", ca: certmagic.GoogleTrustProductionCA, want: "Google Trust Services"},
|
||||||
|
{name: "Google Trust Services staging", ca: certmagic.GoogleTrustStagingCA, want: "Google Trust Services (staging)"},
|
||||||
|
{name: "other", ca: "https://ca.example/directory", want: "https://ca.example/directory"},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want, formatIssuer(api.CertificateIssuerData{
|
||||||
|
ACME: &api.ACMEIssuerData{CA: tt.ca},
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "unknown", formatIssuer(api.CertificateIssuerData{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatExpiry_UTC(t *testing.T) {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
brisbane := time.FixedZone("AEST", 10*60*60)
|
||||||
|
expires := time.Date(now.Year(), now.Month(), now.Day()+3, 3, 0, 0, 0, brisbane)
|
||||||
|
assert.True(t, strings.HasPrefix(formatExpiry(expires), expires.UTC().Format(time.DateOnly)+" ("))
|
||||||
|
assert.NotEqual(t, expires.Format(time.DateOnly), expires.UTC().Format(time.DateOnly))
|
||||||
|
expired := now.Add(-2 * time.Hour)
|
||||||
|
assert.True(t, strings.HasPrefix(formatExpiry(expired), expired.UTC().Format(time.DateOnly)+" (expired "))
|
||||||
|
assert.Contains(t, formatExpiry(expired), " ago)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIssuedCertificate(name, raw string, expires time.Time, ca string) api.IssuedCertificate {
|
||||||
|
cert := api.IssuedCertificate{
|
||||||
|
SAN: name,
|
||||||
|
Chain: []*x509.Certificate{{Raw: []byte(raw), NotAfter: expires}},
|
||||||
|
}
|
||||||
|
if ca != "" {
|
||||||
|
cert.IssuerData.ACME = &api.ACMEIssuerData{CA: ca}
|
||||||
|
}
|
||||||
|
return cert
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFingerprint(raw string) string {
|
||||||
|
sum := sha256.Sum256([]byte(raw))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import "github.com/spf13/cobra"
|
||||||
|
|
||||||
|
func NewRootCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "cert",
|
||||||
|
Short: "Inspect certificates in cluster storage for Caddy.",
|
||||||
|
}
|
||||||
|
cmd.AddCommand(NewListCommand())
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
+10
-15
@@ -3,17 +3,18 @@ package caddy
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
|
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/alecthomas/chroma/v2/quick"
|
"github.com/alecthomas/chroma/v2/quick"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/internal/cli/completion"
|
"github.com/psviderski/uncloud/internal/cli/completion"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
type configOptions struct {
|
type configOptions struct {
|
||||||
machine string
|
machine string
|
||||||
noColor bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfigCommand() *cobra.Command {
|
func NewConfigCommand() *cobra.Command {
|
||||||
@@ -31,8 +32,6 @@ func NewConfigCommand() *cobra.Command {
|
|||||||
|
|
||||||
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
||||||
"Name or ID of the machine to get the configuration from. (default is connected machine)")
|
"Name or ID of the machine to get the configuration from. (default is connected machine)")
|
||||||
cmd.Flags().BoolVar(&opts.noColor, "no-color", false,
|
|
||||||
"Disable syntax highlighting for the output.")
|
|
||||||
|
|
||||||
completion.MachinesFlag(cmd)
|
completion.MachinesFlag(cmd)
|
||||||
|
|
||||||
@@ -46,24 +45,20 @@ func runConfig(ctx context.Context, uncli *cli.CLI, opts configOptions) error {
|
|||||||
}
|
}
|
||||||
defer clusterClient.Close()
|
defer clusterClient.Close()
|
||||||
|
|
||||||
if opts.machine != "" {
|
config, err := clusterClient.Caddy.Config(ctx, client.CaddyConfigOptions{Machine: opts.machine})
|
||||||
// If a specific machine is requested, use it to get the Caddy configuration.
|
|
||||||
ctx = clusterClient.ProxySingleMachineContext(ctx, opts.machine)
|
|
||||||
}
|
|
||||||
|
|
||||||
config, err := clusterClient.Caddy.GetConfig(ctx, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get Caddy config: %w", err)
|
return fmt.Errorf("get Caddy config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print the Caddyfile with syntax highlighting.
|
// Print the Caddyfile with syntax highlighting.
|
||||||
if opts.noColor {
|
if err = quick.Highlight(lipgloss.Writer, config.Caddyfile, "caddy", "terminal256", "monokai"); err != nil {
|
||||||
fmt.Print(config.Caddyfile)
|
|
||||||
} else {
|
|
||||||
if err = quick.Highlight(os.Stdout, config.Caddyfile, "caddy", "terminal256", "monokai"); err != nil {
|
|
||||||
// If highlighting fails, fall back to plain output.
|
// If highlighting fails, fall back to plain output.
|
||||||
fmt.Print(config.Caddyfile)
|
lipgloss.Print(config.Caddyfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.LastReconciliationError != "" {
|
||||||
|
tui.PrintWarning(fmt.Sprintf("last Caddy config load failed: %s\nShowing the last saved Caddyfile.",
|
||||||
|
config.LastReconciliationError))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+197
-19
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/internal/cli/completion"
|
"github.com/psviderski/uncloud/internal/cli/completion"
|
||||||
"github.com/psviderski/uncloud/internal/cli/tui"
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
|
"github.com/psviderski/uncloud/internal/version"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client"
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -25,6 +26,10 @@ type deployOptions struct {
|
|||||||
caddyfile string
|
caddyfile string
|
||||||
image string
|
image string
|
||||||
machines []string
|
machines []string
|
||||||
|
|
||||||
|
caddyfileChanged bool
|
||||||
|
imageChanged bool
|
||||||
|
machinesChanged bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDeployCommand() *cobra.Command {
|
func NewDeployCommand() *cobra.Command {
|
||||||
@@ -33,21 +38,62 @@ func NewDeployCommand() *cobra.Command {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "deploy",
|
Use: "deploy",
|
||||||
Short: "Deploy or upgrade Caddy reverse proxy across all machines in the cluster.",
|
Short: "Deploy or upgrade Caddy reverse proxy across all machines in the cluster.",
|
||||||
Long: "Deploy or upgrade Caddy reverse proxy across all machines in the cluster.\n" +
|
Long: `Deploy or upgrade Caddy reverse proxy across all machines in the cluster.
|
||||||
"A rolling update is performed when updating existing containers to minimise disruption.",
|
A rolling update is performed when updating existing containers to minimise disruption.
|
||||||
|
|
||||||
|
By default, this deploys the latest version of the Uncloud Caddy image with a global config that enables cluster
|
||||||
|
storage for TLS certificates. See https://github.com/unlabs-dev/caddy-uncloud for more details.
|
||||||
|
|
||||||
|
Values set with --image, --caddyfile, and --machine are kept for later deployments. Pass an empty value to reset
|
||||||
|
a flag to its default.
|
||||||
|
|
||||||
|
If this command didn't deploy the current caddy service, for example, it was deployed with 'uc deploy'
|
||||||
|
from a Compose file or an older version of uc, the deployment replaces its configuration.
|
||||||
|
|
||||||
|
Requires Uncloud daemon ` + client.MinCaddyDaemonVersion + ` or newer on all machines.`,
|
||||||
|
Example: ` # Deploy Caddy or upgrade it to the latest version of the Uncloud Caddy image.
|
||||||
|
uc caddy deploy
|
||||||
|
|
||||||
|
# Deploy a custom image and keep it for later deployments.
|
||||||
|
uc caddy deploy --image ghcr.io/unlabs-dev/caddy-uncloud:0.1.3
|
||||||
|
|
||||||
|
# Go back to the latest version of the Uncloud Caddy image.
|
||||||
|
uc caddy deploy --image ""
|
||||||
|
|
||||||
|
# Deploy a custom image build and a custom global Caddy config and keep both for later deployments.
|
||||||
|
uc caddy deploy --image myorg/caddy-uncloud:latest --caddyfile global.Caddyfile
|
||||||
|
|
||||||
|
# Go back to the default global Caddy config but keep the custom image.
|
||||||
|
uc caddy deploy --caddyfile ""
|
||||||
|
|
||||||
|
# Deploy only to specific machines and keep them for later deployments.
|
||||||
|
uc caddy deploy --machine machine1,machine2
|
||||||
|
|
||||||
|
# Go back to deploying to all machines.
|
||||||
|
uc caddy deploy --machine ""`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
|
opts.caddyfileChanged = cmd.Flag("caddyfile").Changed
|
||||||
|
opts.imageChanged = cmd.Flag("image").Changed
|
||||||
|
opts.machinesChanged = cmd.Flag("machine").Changed
|
||||||
|
|
||||||
return runDeploy(cmd.Context(), uncli, opts)
|
return runDeploy(cmd.Context(), uncli, opts)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVar(&opts.caddyfile, "caddyfile", "",
|
cmd.Flags().StringVar(&opts.caddyfile, "caddyfile", "",
|
||||||
"Path to a custom global Caddy config (Caddyfile) that will be prepended to the auto-generated Caddy config.")
|
"Path to a custom global Caddy config (Caddyfile) that will be prepended to the auto-generated Caddy config.\n"+
|
||||||
|
"An empty value resets it to the default config that enables cluster storage.\n"+
|
||||||
|
"(default is the current custom config or the default config)")
|
||||||
cmd.Flags().StringVar(&opts.image, "image", "",
|
cmd.Flags().StringVar(&opts.image, "image", "",
|
||||||
"Caddy Docker image to deploy. (default caddy:LATEST_VERSION)")
|
"Caddy Docker image to deploy. It must include the Uncloud storage module if the Caddy config uses it.\n"+
|
||||||
|
"An empty value resets it to the latest version of the Uncloud Caddy image.\n"+
|
||||||
|
"(default is the current custom image or "+client.CaddyImage+":LATEST_VERSION)")
|
||||||
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||||
"Machine names or IDs to deploy to. Can be specified multiple times or as a comma-separated "+
|
"Machine names or IDs to deploy to. Can be specified multiple times or as a comma-separated list.\n"+
|
||||||
"list. (default is all machines)")
|
"An empty value resets it to all machines.\n"+
|
||||||
|
"(default is the current machines or all machines)")
|
||||||
|
|
||||||
completion.MachinesFlag(cmd)
|
completion.MachinesFlag(cmd)
|
||||||
|
|
||||||
@@ -55,13 +101,13 @@ func NewDeployCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||||
caddyfile := ""
|
config := ""
|
||||||
if opts.caddyfile != "" {
|
if opts.caddyfile != "" {
|
||||||
data, err := os.ReadFile(opts.caddyfile)
|
data, err := os.ReadFile(opts.caddyfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("read Caddyfile: %w", err)
|
return fmt.Errorf("read Caddyfile: %w", err)
|
||||||
}
|
}
|
||||||
caddyfile = strings.TrimSpace(string(data))
|
config = strings.TrimSpace(string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
clusterClient, err := uncli.ConnectCluster(ctx)
|
clusterClient, err := uncli.ConnectCluster(ctx)
|
||||||
@@ -70,6 +116,10 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
}
|
}
|
||||||
defer clusterClient.Close()
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
if err = clusterClient.Caddy.CheckDaemonVersions(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
svc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
svc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, api.ErrNotFound) {
|
if !errors.Is(err, api.ErrNotFound) {
|
||||||
@@ -100,24 +150,48 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.image != "" {
|
current := svc.Spec()
|
||||||
fmt.Println(tui.Faint.Render("target image: ") + tui.FormatImage(opts.image, tui.Green))
|
deployOpts := resolveDeploymentOptions(current, opts, config)
|
||||||
|
configChanged := deployOpts.ConfigOrDefault() != current.CaddyConfig()
|
||||||
|
|
||||||
|
if svc.ID != "" && configChanged {
|
||||||
|
printCaddyConfig("current global Caddy config:", current.CaddyConfig())
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("Preparing a deployment plan...")
|
fmt.Println("Preparing a deployment plan...")
|
||||||
|
|
||||||
placement := api.Placement{
|
d, err := clusterClient.Caddy.NewDeployment(ctx, deployOpts)
|
||||||
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
|
|
||||||
}
|
|
||||||
d, err := clusterClient.NewCaddyDeployment(opts.image, caddyfile, placement)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create caddy deployment: %w", err)
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
target := d.Spec
|
||||||
|
|
||||||
if opts.image == "" {
|
// The Uncloud Caddy image resolves to the latest version allowed by this CLI, which is older than the running image
|
||||||
fmt.Println(tui.Faint.Render("target image: ") + tui.FormatImage(d.Spec.Container.Image,
|
// if a newer CLI deployed it. Continuing would downgrade the image and replace its default config.
|
||||||
tui.Green) + tui.Faint.Render(" (latest stable)"))
|
if deployOpts.Image == "" && client.IsNewerCaddyImage(current.Container.Image, target.Container.Image) {
|
||||||
|
return fmt.Errorf("caddy service runs image '%s' which is newer than the latest version '%s' supported by "+
|
||||||
|
"this uc version: upgrade uc or specify the image with --image",
|
||||||
|
current.Container.Image, target.Container.Image)
|
||||||
|
}
|
||||||
|
|
||||||
|
imageNote := " (latest)"
|
||||||
|
if deployOpts.Image != "" {
|
||||||
|
imageNote = " (custom)"
|
||||||
|
if !opts.imageChanged {
|
||||||
|
imageNote = " (custom, use --image \"\" for the latest version)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println(tui.Faint.Render("target image: ") + tui.FormatImage(target.Container.Image, tui.Green) +
|
||||||
|
tui.Faint.Render(imageNote))
|
||||||
|
|
||||||
|
if configChanged {
|
||||||
|
if deployOpts.Config != nil {
|
||||||
|
// The user provided this config so there is no need to print its content.
|
||||||
|
fmt.Println(tui.Faint.Render("target global Caddy config: ") + opts.caddyfile)
|
||||||
|
} else {
|
||||||
|
printCaddyConfig("target global Caddy config: (default)", deployOpts.ConfigOrDefault())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plan, err := d.Plan(ctx)
|
plan, err := d.Plan(ctx)
|
||||||
@@ -129,7 +203,7 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
|
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
|
||||||
} else {
|
} else {
|
||||||
if svc.ID == "" {
|
if svc.ID == "" {
|
||||||
if len(opts.machines) > 0 {
|
if len(deployOpts.Placement.Machines) > 0 {
|
||||||
fmt.Println("This will run a Caddy container on each selected machine.")
|
fmt.Println("This will run a Caddy container on each selected machine.")
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("This will run a Caddy container on each machine.")
|
fmt.Println("This will run a Caddy container on each machine.")
|
||||||
@@ -161,6 +235,14 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
fmt.Println(summary)
|
fmt.Println(summary)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
|
warnings := deploymentWarnings(current, target, opts)
|
||||||
|
for _, warning := range warnings {
|
||||||
|
tui.PrintWarning(warning)
|
||||||
|
}
|
||||||
|
if len(warnings) > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
title := "Proceed with deployment?"
|
title := "Proceed with deployment?"
|
||||||
// Include the direct connection or context name in the confirmation prompt to avoid accidentally
|
// Include the direct connection or context name in the confirmation prompt to avoid accidentally
|
||||||
// deploying to the wrong cluster.
|
// deploying to the wrong cluster.
|
||||||
@@ -193,6 +275,98 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
return UpdateDomainRecords(ctx, clusterClient, uncli.ProgressOut())
|
return UpdateDomainRecords(ctx, clusterClient, uncli.ProgressOut())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveDeploymentOptions returns the deployment options for the flags and the current service spec, which is zero if
|
||||||
|
// the service doesn't exist. Each flag works on its own: a specified flag overrides the value and an empty one resets
|
||||||
|
// it to the default. A flag that isn't specified keeps the explicitly set value.
|
||||||
|
func resolveDeploymentOptions(
|
||||||
|
current api.ServiceSpec, opts deployOptions, config string,
|
||||||
|
) client.CaddyDeploymentOptions {
|
||||||
|
var resolved client.CaddyDeploymentOptions
|
||||||
|
|
||||||
|
if opts.imageChanged {
|
||||||
|
resolved.Image = opts.image
|
||||||
|
} else if _, custom := current.Labels[api.LabelCaddyImageCustom]; custom {
|
||||||
|
resolved.Image = current.Container.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.caddyfileChanged {
|
||||||
|
if opts.caddyfile != "" {
|
||||||
|
resolved.Config = new(config)
|
||||||
|
}
|
||||||
|
} else if _, custom := current.Labels[api.LabelCaddyConfigCustom]; custom {
|
||||||
|
resolved.Config = new(current.CaddyConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.machinesChanged {
|
||||||
|
resolved.Placement.Machines = cli.ExpandCommaSeparatedValues(opts.machines)
|
||||||
|
} else if _, managed := current.Labels[api.LabelCaddyManaged]; managed {
|
||||||
|
resolved.Placement = current.Placement
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
// deploymentWarnings returns the consequences of deploying the target spec over the current one, which is zero if
|
||||||
|
// the service doesn't exist.
|
||||||
|
func deploymentWarnings(current, target api.ServiceSpec, opts deployOptions) []string {
|
||||||
|
var warnings []string
|
||||||
|
exists := current.Name != ""
|
||||||
|
|
||||||
|
if _, managed := current.Labels[api.LabelCaddyManaged]; exists && !managed {
|
||||||
|
warnings = append(warnings, "'uc caddy deploy' doesn't manage this caddy service yet, so this deployment "+
|
||||||
|
"replaces its configuration, including any custom global Caddy config.\n"+
|
||||||
|
"If you manage it with a Compose file, cancel and deploy with 'uc deploy' instead.")
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStorage := client.CaddyStorageModule(current.CaddyConfig())
|
||||||
|
targetStorage := client.CaddyStorageModule(target.CaddyConfig())
|
||||||
|
if exists && currentStorage != targetStorage {
|
||||||
|
warning := fmt.Sprintf("Caddy certificate storage changes from %s to %s.\n"+
|
||||||
|
"Caddy doesn't automatically migrate certificates, so all of them will be reissued and HTTPS may fail "+
|
||||||
|
"for those domains until that finishes.\n"+
|
||||||
|
"To migrate them first, see %s/concepts/ingress/cluster-storage-for-caddy",
|
||||||
|
caddyStorageName(currentStorage), caddyStorageName(targetStorage), version.DocsURL)
|
||||||
|
if !opts.caddyfileChanged {
|
||||||
|
warning += "\nTo keep the current storage, specify a custom Caddyfile that uses it with --caddyfile."
|
||||||
|
}
|
||||||
|
warnings = append(warnings, warning)
|
||||||
|
}
|
||||||
|
|
||||||
|
imageOrConfigChanged := current.Container.Image != target.Container.Image ||
|
||||||
|
current.CaddyConfig() != target.CaddyConfig()
|
||||||
|
if targetStorage == client.CaddyStorageUncloud && imageOrConfigChanged &&
|
||||||
|
!client.IsDefaultCaddyImage(target.Container.Image) {
|
||||||
|
warnings = append(warnings, fmt.Sprintf("Image '%s' must include the Uncloud storage module to use "+
|
||||||
|
"'storage uncloud'. Otherwise, Caddy won't start or apply the new config.\n"+
|
||||||
|
"Check 'uc caddy config' after the deployment for errors.", target.Container.Image))
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
}
|
||||||
|
|
||||||
|
// caddyStorageName returns a user-friendly name of the Caddy storage module.
|
||||||
|
func caddyStorageName(module string) string {
|
||||||
|
switch module {
|
||||||
|
case client.CaddyStorageFileSystem:
|
||||||
|
return "local"
|
||||||
|
case client.CaddyStorageUncloud:
|
||||||
|
return "cluster"
|
||||||
|
default:
|
||||||
|
return "'" + module + "'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printCaddyConfig prints the title and the indented config or "none" if it's empty.
|
||||||
|
func printCaddyConfig(title, config string) {
|
||||||
|
if config == "" {
|
||||||
|
lipgloss.Println(tui.Faint.Render(title) + " none")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lipgloss.Println(tui.Faint.Render(title))
|
||||||
|
fmt.Println(config)
|
||||||
|
}
|
||||||
|
|
||||||
func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, progressOut *streams.Out) error {
|
func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, progressOut *streams.Out) error {
|
||||||
domain, err := clusterClient.GetDomain(ctx)
|
domain, err := clusterClient.GetDomain(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -202,6 +376,10 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
|
|||||||
}
|
}
|
||||||
return fmt.Errorf("get cluster domain: %w", err)
|
return fmt.Errorf("get cluster domain: %w", err)
|
||||||
}
|
}
|
||||||
|
if !domain.Reserved {
|
||||||
|
fmt.Printf("Skipping DNS records update as cluster domain '%s' is managed externally.\n", domain.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("Updating cluster domain records in Uncloud DNS to point to machines running caddy service...")
|
fmt.Println("Updating cluster domain records in Uncloud DNS to point to machines running caddy service...")
|
||||||
// TODO: split the method into two: one to get the records and one to update them to ask for update confirmation.
|
// TODO: split the method into two: one to get the records and one to update them to ask for update confirmation.
|
||||||
@@ -216,7 +394,7 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
|
|||||||
if errors.Is(err, client.ErrNoReachableMachines) {
|
if errors.Is(err, client.ErrNoReachableMachines) {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("DNS records for domain '%s' could not be updated as there are no internet-reachable "+
|
fmt.Printf("DNS records for domain '%s' could not be updated as there are no internet-reachable "+
|
||||||
"machines running caddy containers.\n", domain)
|
"machines running caddy containers.\n", domain.Name)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("Possible solutions:")
|
fmt.Println("Possible solutions:")
|
||||||
fmt.Println("- Ensure your machines have public IP addresses")
|
fmt.Println("- Ensure your machines have public IP addresses")
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
package caddy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveDeploymentOptions(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
customImage = "example.com/caddy:1.2.3"
|
||||||
|
customConfig = "{\n\tdebug\n}"
|
||||||
|
)
|
||||||
|
machines := api.Placement{Machines: []string{"m1", "m2"}}
|
||||||
|
|
||||||
|
// A service with a custom image, config, and machines that has the given labels.
|
||||||
|
service := func(labels ...string) api.ServiceSpec {
|
||||||
|
spec := api.ServiceSpec{
|
||||||
|
Name: client.CaddyServiceName,
|
||||||
|
Caddy: &api.CaddySpec{Config: customConfig},
|
||||||
|
Container: api.ContainerSpec{Image: customImage},
|
||||||
|
Placement: machines,
|
||||||
|
}
|
||||||
|
if len(labels) > 0 {
|
||||||
|
spec.Labels = make(map[string]string, len(labels))
|
||||||
|
for _, label := range labels {
|
||||||
|
spec.Labels[label] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
current api.ServiceSpec
|
||||||
|
opts deployOptions
|
||||||
|
config string
|
||||||
|
want client.CaddyDeploymentOptions
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no service gets the defaults",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service that isn't managed gets the defaults",
|
||||||
|
current: service(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "managed service follows the default image and config and keeps machines",
|
||||||
|
current: service(api.LabelCaddyManaged),
|
||||||
|
want: client.CaddyDeploymentOptions{Placement: machines},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom image is kept and config follows the default",
|
||||||
|
current: service(api.LabelCaddyManaged, api.LabelCaddyImageCustom),
|
||||||
|
want: client.CaddyDeploymentOptions{Image: customImage, Placement: machines},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom config is kept and image follows the latest version",
|
||||||
|
current: service(api.LabelCaddyManaged, api.LabelCaddyConfigCustom),
|
||||||
|
want: client.CaddyDeploymentOptions{Config: new(customConfig), Placement: machines},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom image and config are kept",
|
||||||
|
current: service(api.LabelCaddyManaged, api.LabelCaddyImageCustom, api.LabelCaddyConfigCustom),
|
||||||
|
want: client.CaddyDeploymentOptions{Image: customImage, Config: new(customConfig), Placement: machines},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom empty config is kept",
|
||||||
|
current: api.ServiceSpec{
|
||||||
|
Name: client.CaddyServiceName,
|
||||||
|
Labels: map[string]string{api.LabelCaddyManaged: "", api.LabelCaddyConfigCustom: ""},
|
||||||
|
},
|
||||||
|
want: client.CaddyDeploymentOptions{Config: new("")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "image flag overrides the current image",
|
||||||
|
current: service(api.LabelCaddyManaged, api.LabelCaddyImageCustom),
|
||||||
|
opts: deployOptions{image: "caddy:2", imageChanged: true},
|
||||||
|
want: client.CaddyDeploymentOptions{Image: "caddy:2", Placement: machines},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty image flag resets to the latest version",
|
||||||
|
current: service(api.LabelCaddyManaged, api.LabelCaddyImageCustom),
|
||||||
|
opts: deployOptions{imageChanged: true},
|
||||||
|
want: client.CaddyDeploymentOptions{Placement: machines},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "caddyfile flag overrides the current config",
|
||||||
|
current: service(api.LabelCaddyManaged),
|
||||||
|
opts: deployOptions{caddyfile: "global.Caddyfile", caddyfileChanged: true},
|
||||||
|
config: "{\n\tstorage redis\n}",
|
||||||
|
want: client.CaddyDeploymentOptions{Config: new("{\n\tstorage redis\n}"), Placement: machines},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "caddyfile flag with an empty file sets a custom empty config",
|
||||||
|
current: service(api.LabelCaddyManaged),
|
||||||
|
opts: deployOptions{caddyfile: "empty.Caddyfile", caddyfileChanged: true},
|
||||||
|
want: client.CaddyDeploymentOptions{Config: new(""), Placement: machines},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty caddyfile flag resets to the default config",
|
||||||
|
current: service(api.LabelCaddyManaged, api.LabelCaddyConfigCustom),
|
||||||
|
opts: deployOptions{caddyfileChanged: true},
|
||||||
|
want: client.CaddyDeploymentOptions{Placement: machines},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "machine flag overrides the current machines",
|
||||||
|
current: service(api.LabelCaddyManaged),
|
||||||
|
opts: deployOptions{machines: []string{"m3,m4", "m5"}, machinesChanged: true},
|
||||||
|
want: client.CaddyDeploymentOptions{Placement: api.Placement{Machines: []string{"m3", "m4", "m5"}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty machine flag resets to all machines",
|
||||||
|
current: service(api.LabelCaddyManaged),
|
||||||
|
opts: deployOptions{machines: []string{}, machinesChanged: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "flags apply to a service that isn't managed",
|
||||||
|
current: service(),
|
||||||
|
opts: deployOptions{
|
||||||
|
image: "caddy:2", imageChanged: true,
|
||||||
|
caddyfile: "global.Caddyfile", caddyfileChanged: true,
|
||||||
|
machines: []string{"m3"}, machinesChanged: true,
|
||||||
|
},
|
||||||
|
config: customConfig,
|
||||||
|
want: client.CaddyDeploymentOptions{
|
||||||
|
Image: "caddy:2",
|
||||||
|
Config: new(customConfig),
|
||||||
|
Placement: api.Placement{Machines: []string{"m3"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want, resolveDeploymentOptions(tt.current, tt.opts, tt.config))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeploymentWarnings(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultImage = client.CaddyImage + ":0.1.3"
|
||||||
|
customImage = "example.com/caddy:1.2.3"
|
||||||
|
localConfig = "{\n\tdebug\n}"
|
||||||
|
override = "doesn't manage this caddy service yet"
|
||||||
|
storage = "certificate storage changes from"
|
||||||
|
keepStorage = "To keep the current storage"
|
||||||
|
module = "must include the Uncloud storage module"
|
||||||
|
)
|
||||||
|
managed := map[string]string{api.LabelCaddyManaged: ""}
|
||||||
|
|
||||||
|
service := func(image, config string, labels map[string]string) api.ServiceSpec {
|
||||||
|
spec := api.ServiceSpec{
|
||||||
|
Name: client.CaddyServiceName,
|
||||||
|
Container: api.ContainerSpec{Image: image},
|
||||||
|
Labels: labels,
|
||||||
|
}
|
||||||
|
if config != "" {
|
||||||
|
spec.Caddy = &api.CaddySpec{Config: config}
|
||||||
|
}
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
current api.ServiceSpec
|
||||||
|
target api.ServiceSpec
|
||||||
|
opts deployOptions
|
||||||
|
want []string
|
||||||
|
notWant []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "new service with defaults has no warnings",
|
||||||
|
target: service(defaultImage, client.DefaultCaddyConfig, managed),
|
||||||
|
notWant: []string{override, storage, module},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "new service with a custom image",
|
||||||
|
target: service(customImage, client.DefaultCaddyConfig, managed),
|
||||||
|
want: []string{module},
|
||||||
|
notWant: []string{override, storage},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service that isn't managed moves to cluster storage",
|
||||||
|
current: service("caddy:2.11.4", "", nil),
|
||||||
|
target: service(defaultImage, client.DefaultCaddyConfig, managed),
|
||||||
|
want: []string{override, "from local to cluster", keepStorage},
|
||||||
|
notWant: []string{module},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service that isn't managed already uses cluster storage",
|
||||||
|
current: service(defaultImage, client.DefaultCaddyConfig, nil),
|
||||||
|
target: service(defaultImage, client.DefaultCaddyConfig, managed),
|
||||||
|
want: []string{override},
|
||||||
|
notWant: []string{storage, module},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "managed service without changes has no warnings",
|
||||||
|
current: service(defaultImage, client.DefaultCaddyConfig, managed),
|
||||||
|
target: service(defaultImage, client.DefaultCaddyConfig, managed),
|
||||||
|
notWant: []string{override, storage, module},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "managed service upgrades the default image",
|
||||||
|
current: service(client.CaddyImage+":0.1.2", client.DefaultCaddyConfig, managed),
|
||||||
|
target: service(defaultImage, client.DefaultCaddyConfig, managed),
|
||||||
|
notWant: []string{override, storage, module},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom caddyfile moves to local storage",
|
||||||
|
current: service(defaultImage, client.DefaultCaddyConfig, managed),
|
||||||
|
target: service(defaultImage, localConfig, managed),
|
||||||
|
opts: deployOptions{caddyfile: "global.Caddyfile", caddyfileChanged: true},
|
||||||
|
want: []string{"from cluster to local"},
|
||||||
|
notWant: []string{override, keepStorage, module},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other storage module is named",
|
||||||
|
current: service(defaultImage, "{\n\tstorage redis\n}", managed),
|
||||||
|
target: service(defaultImage, client.DefaultCaddyConfig, managed),
|
||||||
|
want: []string{"from 'redis' to cluster", keepStorage},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom image is unchanged with the same config",
|
||||||
|
current: service(customImage, client.DefaultCaddyConfig, managed),
|
||||||
|
target: service(customImage, client.DefaultCaddyConfig, managed),
|
||||||
|
notWant: []string{override, storage, module},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom image receives a changed default config",
|
||||||
|
current: service(customImage, "{\n\tstorage uncloud\n\tdebug\n}", managed),
|
||||||
|
target: service(customImage, client.DefaultCaddyConfig, managed),
|
||||||
|
want: []string{module},
|
||||||
|
notWant: []string{override, storage},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom image with local storage",
|
||||||
|
current: service(defaultImage, localConfig, managed),
|
||||||
|
target: service(customImage, localConfig, managed),
|
||||||
|
notWant: []string{override, storage, module},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
warnings := strings.Join(deploymentWarnings(tt.current, tt.target, tt.opts), "\n")
|
||||||
|
for _, want := range tt.want {
|
||||||
|
assert.Contains(t, warnings, want)
|
||||||
|
}
|
||||||
|
for _, notWant := range tt.notWant {
|
||||||
|
assert.NotContains(t, warnings, notWant)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package caddy
|
package caddy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/psviderski/uncloud/cmd/uc/caddy/cert"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ func NewRootCommand() *cobra.Command {
|
|||||||
Short: "Manage Caddy reverse proxy service.",
|
Short: "Manage Caddy reverse proxy service.",
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
|
cert.NewRootCommand(),
|
||||||
NewConfigCommand(),
|
NewConfigCommand(),
|
||||||
NewDeployCommand(),
|
NewDeployCommand(),
|
||||||
NewLogsCommand(),
|
NewLogsCommand(),
|
||||||
|
|||||||
+1
-10
@@ -22,15 +22,6 @@ func NewShowCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func show(uncli *cli.CLI) error {
|
func show(uncli *cli.CLI) error {
|
||||||
// discard errors, only show the current context, otherwise nothing
|
fmt.Println(uncli.ContextOverrideOrCurrent())
|
||||||
if uncli.Config == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(uncli.Config.Contexts) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
fmt.Println(uncli.Config.CurrentContext)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ import (
|
|||||||
"github.com/psviderski/uncloud/pkg/client"
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/psviderski/uncloud/pkg/client/compose"
|
"github.com/psviderski/uncloud/pkg/client/compose"
|
||||||
"github.com/psviderski/uncloud/pkg/client/deploy"
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
"github.com/psviderski/uncloud/pkg/client/deploy/operation"
|
"github.com/psviderski/uncloud/pkg/client/operation"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -305,7 +305,7 @@ const defaultFailedContainerLogsTail = 10
|
|||||||
// UNCLOUD_FAILED_CONTAINER_LOGS_TAIL environment variable override when set and valid.
|
// UNCLOUD_FAILED_CONTAINER_LOGS_TAIL environment variable override when set and valid.
|
||||||
func failedContainerLogsTail() int {
|
func failedContainerLogsTail() int {
|
||||||
if v := os.Getenv("UNCLOUD_FAILED_CONTAINER_LOGS_TAIL"); v != "" {
|
if v := os.Getenv("UNCLOUD_FAILED_CONTAINER_LOGS_TAIL"); v != "" {
|
||||||
if tail, err := logs.Tail(v); err == nil && (tail == -1 || tail > 0) {
|
if tail, err := logs.ParseTail(v); err == nil && (tail == -1 || tail > 0) {
|
||||||
return tail
|
return tail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func release(ctx context.Context, uncli *cli.CLI) error {
|
|||||||
if status.Convert(err).Code() == codes.NotFound {
|
if status.Convert(err).Code() == codes.NotFound {
|
||||||
return errors.New("no domain reserved")
|
return errors.New("no domain reserved")
|
||||||
}
|
}
|
||||||
|
return fmt.Errorf("release cluster domain: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Released cluster domain: %s\n", domain.Name)
|
fmt.Printf("Released cluster domain: %s\n", domain.Name)
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ func reserve(ctx context.Context, uncli *cli.CLI, opts reserveOptions) error {
|
|||||||
domain, err := clusterClient.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.endpoint})
|
domain, err := clusterClient.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.endpoint})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if status.Convert(err).Code() == codes.AlreadyExists {
|
if status.Convert(err).Code() == codes.AlreadyExists {
|
||||||
return errors.New("domain already reserved")
|
return errors.New("cluster domain already configured, unset it with 'uc dns set \"\"' " +
|
||||||
|
"or release a reservation with 'uc dns release' first")
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-3
@@ -7,16 +7,19 @@ import (
|
|||||||
func NewRootCommand() *cobra.Command {
|
func NewRootCommand() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "dns",
|
Use: "dns",
|
||||||
Short: "Manage cluster domain in Uncloud DNS.",
|
Short: "Manage the cluster domain.",
|
||||||
Long: "Manage cluster domain in Uncloud DNS.\n" +
|
Long: "Manage the cluster domain.\n" +
|
||||||
"DNS commands allow you to reserve or release a unique 'xxxxxx.uncld.dev' domain for your " +
|
"DNS commands allow you to reserve or release a unique 'xxxxxx.uncld.dev' domain for your " +
|
||||||
"cluster. When reserved, Caddy service deployments will automatically update DNS records to route " +
|
"cluster. When reserved, Caddy service deployments will automatically update DNS records to route " +
|
||||||
"traffic to the services in the cluster.",
|
"traffic to the services in the cluster.\n\n" +
|
||||||
|
"EXPERIMENTAL: Use 'uc dns set' to configure an externally managed domain instead. " +
|
||||||
|
"Uncloud does not manage DNS records for manually set domains.",
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
NewReleaseCommand(),
|
NewReleaseCommand(),
|
||||||
NewReserveCommand(),
|
NewReserveCommand(),
|
||||||
NewShowCommand(),
|
NewShowCommand(),
|
||||||
|
NewSetCommand(),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/psviderski/uncloud/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
|
"github.com/psviderski/uncloud/internal/grpcversion"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSetCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "set DOMAIN_NAME",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Short: "Set or unset an externally managed cluster domain (EXPERIMENTAL).",
|
||||||
|
Long: "EXPERIMENTAL: Set the cluster domain used to generate ingress hostnames for services.\n" +
|
||||||
|
"Configure wildcard DNS records for this domain with your DNS provider. " +
|
||||||
|
"Uncloud will not create, verify, update, or delete external DNS records.\n" +
|
||||||
|
"Pass an empty string to unset a manually set domain. " +
|
||||||
|
"Use 'uc dns release' to release a domain reserved in Uncloud DNS. " +
|
||||||
|
"Setting or unsetting the domain does not change existing service hostnames.",
|
||||||
|
Example: " uc dns set apps.example.com\n uc dns set \"\"",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return set(cmd.Context(), uncli, args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func set(ctx context.Context, uncli *cli.CLI, name string) error {
|
||||||
|
if name != "" {
|
||||||
|
if labels, ok := dns.IsDomainName(name); !ok || labels < 2 {
|
||||||
|
return fmt.Errorf("invalid cluster domain %q: must be a valid domain name with at least two labels", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tui.PrintWarning("Setting an externally managed cluster domain is experimental.")
|
||||||
|
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
ctx = metadata.AppendToOutgoingContext(ctx, grpcversion.MetadataKeyMinServerVersion, "0.21.0")
|
||||||
|
_, err = clusterClient.SetDomain(ctx, &pb.SetDomainRequest{Name: name})
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() == codes.AlreadyExists {
|
||||||
|
return errors.New("cluster domain already configured, unset it with 'uc dns set \"\"' or " +
|
||||||
|
"release a reservation with 'uc dns release' first")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("set cluster domain: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
fmt.Println("Unset cluster domain. External DNS records have not been changed.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Printf("Set cluster domain: %s\n", name)
|
||||||
|
fmt.Println("DNS is managed externally. " +
|
||||||
|
"Configure wildcard DNS records pointing to machines running the Caddy service.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+2
-2
@@ -33,11 +33,11 @@ func show(ctx context.Context, uncli *cli.CLI) error {
|
|||||||
domain, err := clusterClient.GetDomain(ctx)
|
domain, err := clusterClient.GetDomain(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, api.ErrNotFound) {
|
if errors.Is(err, api.ErrNotFound) {
|
||||||
return errors.New("no domain reserved")
|
return errors.New("no cluster domain configured")
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(domain)
|
fmt.Println(domain.Name)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-46
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/charmbracelet/colorprofile"
|
"github.com/charmbracelet/colorprofile"
|
||||||
"github.com/containerd/platforms"
|
"github.com/containerd/platforms"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/go-units"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
@@ -90,10 +91,15 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
|||||||
|
|
||||||
machines := cli.ExpandCommaSeparatedValues(opts.machines)
|
machines := cli.ExpandCommaSeparatedValues(opts.machines)
|
||||||
|
|
||||||
clusterImages, err := clusterClient.ListImages(ctx, api.ImageFilter{
|
var clusterImages []api.MachineImages
|
||||||
|
err = tui.RunSpinner(ctx, "Listing images...", func(ctx context.Context) error {
|
||||||
|
var err error
|
||||||
|
clusterImages, err = clusterClient.ListImages(ctx, api.ImageFilter{
|
||||||
Machines: machines,
|
Machines: machines,
|
||||||
Name: opts.nameFilter,
|
Name: opts.nameFilter,
|
||||||
})
|
})
|
||||||
|
return err
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list images: %w", err)
|
return fmt.Errorf("list images: %w", err)
|
||||||
}
|
}
|
||||||
@@ -102,7 +108,7 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
|||||||
var rows []imageRow
|
var rows []imageRow
|
||||||
|
|
||||||
for _, machineImages := range clusterImages {
|
for _, machineImages := range clusterImages {
|
||||||
if err := machineImages.Error(); err != nil {
|
if err := machineImages.Metadata.Err(); err != nil {
|
||||||
tui.PrintWarning(fmt.Sprintf("failed to list images on machine '%s': %s",
|
tui.PrintWarning(fmt.Sprintf("failed to list images on machine '%s': %s",
|
||||||
machineImages.Metadata.MachineName, err))
|
machineImages.Metadata.MachineName, err))
|
||||||
continue
|
continue
|
||||||
@@ -116,49 +122,7 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
|||||||
store = "containerd"
|
store = "containerd"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each image for this machine.
|
rows = append(rows, imageRows(machineImages.Images, machineName, store)...)
|
||||||
for _, img := range machineImages.Images {
|
|
||||||
// Show the first 12 chars without 'sha256:' as the image ID like Docker does.
|
|
||||||
id := strings.TrimPrefix(img.ID, "sha256:")[:12]
|
|
||||||
|
|
||||||
name := "<none>"
|
|
||||||
if len(img.RepoTags) > 0 && img.RepoTags[0] != "<none>:<none>" {
|
|
||||||
name = img.RepoTags[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
imgPlatforms, _ := imagePlatforms(img)
|
|
||||||
formattedPlatforms := formatPlatforms(imgPlatforms)
|
|
||||||
|
|
||||||
created := ""
|
|
||||||
createdAt := time.Unix(img.Created, 0)
|
|
||||||
if !createdAt.IsZero() {
|
|
||||||
created = units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
|
||||||
}
|
|
||||||
|
|
||||||
size := units.HumanSizeWithPrecision(float64(img.Size), 3)
|
|
||||||
|
|
||||||
// Check if the image is in use by any containers. Only supported by Docker API >=1.51
|
|
||||||
inUse := "-"
|
|
||||||
if img.Containers != -1 { // -1 means the info is not available.
|
|
||||||
if img.Containers > 0 {
|
|
||||||
inUse = lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Render("●")
|
|
||||||
} else {
|
|
||||||
inUse = lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render("○")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rows = append(rows, imageRow{
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
platforms: formattedPlatforms,
|
|
||||||
createdHuman: created,
|
|
||||||
createdUnix: img.Created,
|
|
||||||
size: size,
|
|
||||||
inUse: inUse,
|
|
||||||
store: store,
|
|
||||||
machine: machineName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rows) == 0 {
|
if len(rows) == 0 {
|
||||||
@@ -184,13 +148,55 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// imageRows keeps every stored name visible and retains digest-only and unnamed images.
|
||||||
|
func imageRows(images []image.Summary, machineName, store string) []imageRow {
|
||||||
|
var rows []imageRow
|
||||||
|
for _, img := range images {
|
||||||
|
// RepoTags includes stored digest names on containerd. RepoDigests can include derived names.
|
||||||
|
imageNames := img.RepoTags
|
||||||
|
if len(imageNames) == 0 {
|
||||||
|
imageNames = []string{"<untagged>"}
|
||||||
|
}
|
||||||
|
|
||||||
|
imgPlatforms, _ := imagePlatforms(img)
|
||||||
|
created := ""
|
||||||
|
if img.Created > 0 {
|
||||||
|
created = units.HumanDuration(time.Since(time.Unix(img.Created, 0))) + " ago"
|
||||||
|
}
|
||||||
|
// Check if the image is in use by any containers. Only supported by Docker API >=1.51
|
||||||
|
inUse := "-"
|
||||||
|
if img.Containers >= 0 { // -1 means the info is not available.
|
||||||
|
inUse = lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render("○")
|
||||||
|
if img.Containers > 0 {
|
||||||
|
inUse = lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Render("●")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range imageNames {
|
||||||
|
rows = append(rows, imageRow{
|
||||||
|
id: stringid.TruncateID(img.ID),
|
||||||
|
name: name,
|
||||||
|
platforms: formatPlatforms(imgPlatforms),
|
||||||
|
createdHuman: created,
|
||||||
|
createdUnix: img.Created,
|
||||||
|
size: units.HumanSizeWithPrecision(float64(img.Size), 3),
|
||||||
|
inUse: inUse,
|
||||||
|
store: store,
|
||||||
|
machine: machineName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
// imagePlatforms returns a list of platforms supported by the image and a boolean indicating if it's multi-platform.
|
// imagePlatforms returns a list of platforms supported by the image and a boolean indicating if it's multi-platform.
|
||||||
func imagePlatforms(img image.Summary) ([]string, bool) {
|
func imagePlatforms(img image.Summary) ([]string, bool) {
|
||||||
var formattedPlatforms []string
|
var formattedPlatforms []string
|
||||||
multiPlatform := false
|
multiPlatform := false
|
||||||
|
|
||||||
for _, m := range img.Manifests {
|
for _, m := range img.Manifests {
|
||||||
if m.Kind != image.ManifestKindImage || !m.Available {
|
if m.Kind != image.ManifestKindImage || !m.Available || m.ImageData == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/completion"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type removeOptions struct {
|
||||||
|
machines []string
|
||||||
|
yes bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRemoveCommand() *cobra.Command {
|
||||||
|
opts := removeOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rm IMAGE [IMAGE...]",
|
||||||
|
Aliases: []string{"remove", "delete"},
|
||||||
|
Short: "Remove images from machines in the cluster.",
|
||||||
|
Long: `Remove images from all or selected machines in the cluster.
|
||||||
|
|
||||||
|
An image name without a tag selects all its tags. Specify a tag, such as myapp:latest, to remove
|
||||||
|
only that tag.
|
||||||
|
|
||||||
|
Wildcard patterns are supported for image names and tags and should be quoted to prevent your
|
||||||
|
shell from expanding them. Removing by image ID attempts to remove each name for that image
|
||||||
|
independently.
|
||||||
|
|
||||||
|
Images used by running or stopped containers cannot be removed. Unused tags can be removed even
|
||||||
|
when the same image is used under another tag. Failed removals do not prevent other removals.`,
|
||||||
|
Example: ` # Remove a tag from all machines after confirming the plan.
|
||||||
|
uc image rm myapp:old
|
||||||
|
|
||||||
|
# Remove all tags for an image name from all machines.
|
||||||
|
uc image rm myapp
|
||||||
|
|
||||||
|
# Remove matching tags from two machines without confirmation.
|
||||||
|
uc image rm 'myapp:1.*' 'otherapp:*' -m machine1,machine2 --yes
|
||||||
|
|
||||||
|
# Remove all names for an image by ID from all machines.
|
||||||
|
uc image rm ee8fed0e9618`,
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM")
|
||||||
|
},
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return remove(cmd.Context(), uncli, args, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||||
|
"Machine names or IDs to remove images from. Can be specified multiple times or as a comma-separated list.\n"+
|
||||||
|
"If not specified, the matching images will be removed from all machines.")
|
||||||
|
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
||||||
|
"Auto-confirm the removal plan. Should be explicitly set when running non-interactively,\n"+
|
||||||
|
"e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]")
|
||||||
|
|
||||||
|
completion.MachinesFlag(cmd)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(ctx context.Context, uncli *cli.CLI, images []string, opts removeOptions) error {
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
plan, err := planImageRemoval(ctx, clusterClient, images, cli.ExpandCommaSeparatedValues(opts.machines))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("plan image removal: %w", err)
|
||||||
|
}
|
||||||
|
directConn := uncli.DirectConnection()
|
||||||
|
contextName := uncli.ContextOverrideOrCurrent()
|
||||||
|
cluster := ""
|
||||||
|
if directConn != "" {
|
||||||
|
cluster = directConn
|
||||||
|
lipgloss.Println(tui.Faint.Render("connection: ") + tui.NameStyle.Render(directConn))
|
||||||
|
lipgloss.Println()
|
||||||
|
} else if contextName != "" && len(uncli.Config.Contexts) > 1 {
|
||||||
|
// Only show context if there's more than one to avoid unnecessary clutter.
|
||||||
|
cluster = contextName
|
||||||
|
lipgloss.Println(tui.Faint.Render("context: ") + tui.NameStyle.Render(contextName))
|
||||||
|
lipgloss.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
lipgloss.Println(plan.Format())
|
||||||
|
|
||||||
|
if !opts.yes {
|
||||||
|
if !tui.IsTerminalAvailable() {
|
||||||
|
return errors.New("cannot ask to confirm image removal in non-interactive mode, " +
|
||||||
|
"use --yes flag or set UNCLOUD_AUTO_CONFIRM=true to auto-confirm")
|
||||||
|
}
|
||||||
|
|
||||||
|
title := "Proceed with image removal?"
|
||||||
|
if cluster != "" {
|
||||||
|
isDark := lipgloss.HasDarkBackground(os.Stdin, os.Stdout)
|
||||||
|
confirmStyle := tui.ThemeConfirm().Theme(isDark).Focused.Title
|
||||||
|
title = "Proceed with image removal on " + tui.NameStyle.Render(cluster) + confirmStyle.Render("?")
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmed, err := tui.Confirm(title)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("confirm image removal: %w", err)
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
return cli.Cancelled("Image removal cancelled. No images were removed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
untagged := make(map[string]bool)
|
||||||
|
var errs []error
|
||||||
|
for i, group := range plan.GroupPlans {
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
return group.Execute(ctx, clusterClient, untagged)
|
||||||
|
}, uncli.ProgressOut(), "Removing image "+group.Image)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
"github.com/distribution/reference"
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var imageIDRegex = regexp.MustCompile(`^(sha256:)?[a-f0-9]{4,64}$`)
|
||||||
|
|
||||||
|
// imageRemovalPlan contains all image removal operations grouped by the user-supplied image name, ID, or pattern.
|
||||||
|
type imageRemovalPlan struct {
|
||||||
|
GroupPlans []*removeImageGroupPlan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *imageRemovalPlan) IsEmpty() bool { return len(p.GroupPlans) == 0 }
|
||||||
|
|
||||||
|
func (p *imageRemovalPlan) Format() string {
|
||||||
|
var out strings.Builder
|
||||||
|
machines := make(map[string]struct{})
|
||||||
|
removeCount := 0
|
||||||
|
for _, group := range p.GroupPlans {
|
||||||
|
out.WriteString(group.Format())
|
||||||
|
for _, op := range group.Operations {
|
||||||
|
machines[op.MachineName] = struct{}{}
|
||||||
|
removeCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := fmt.Sprintf("%s remove %s across %s %s",
|
||||||
|
tui.BoldRed.Render(fmt.Sprint(removeCount)),
|
||||||
|
tui.Faint.Render("·"),
|
||||||
|
tui.Bold.Render(fmt.Sprint(len(machines))),
|
||||||
|
pluralMachines(len(machines)),
|
||||||
|
)
|
||||||
|
out.WriteString("\n" + tui.Faint.Render(strings.Repeat("─", lipgloss.Width(summary))) + "\n" + summary + "\n")
|
||||||
|
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluralMachines(count int) string {
|
||||||
|
if count == 1 {
|
||||||
|
return "machine"
|
||||||
|
}
|
||||||
|
return "machines"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute applies the frozen plan, collecting failures without discovering new targets.
|
||||||
|
func (p *imageRemovalPlan) Execute(ctx context.Context, cli api.ImageClient) error {
|
||||||
|
untagged := make(map[string]bool)
|
||||||
|
var errs []error
|
||||||
|
for _, group := range p.GroupPlans {
|
||||||
|
if err := group.Execute(ctx, cli, untagged); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeImageGroupPlan groups removals for one image name, ID, or pattern supplied by the user.
|
||||||
|
type removeImageGroupPlan struct {
|
||||||
|
// Image is the image name, ID, or pattern supplied by the user.
|
||||||
|
Image string
|
||||||
|
Operations []*removeImageOperation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *removeImageGroupPlan) Execute(ctx context.Context, cli api.ImageClient, untagged map[string]bool) error {
|
||||||
|
var errs []error
|
||||||
|
for _, op := range o.Operations {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return errors.Join(append(errs, err)...)
|
||||||
|
}
|
||||||
|
if err := op.Execute(ctx, cli, untagged); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *removeImageGroupPlan) Format() string {
|
||||||
|
var out strings.Builder
|
||||||
|
out.WriteString(tui.BoldRed.Render("-") + " remove image " + tui.NameStyle.Render(o.Image) + "\n")
|
||||||
|
for i, op := range o.Operations {
|
||||||
|
branch := " ├── "
|
||||||
|
if i == len(o.Operations)-1 {
|
||||||
|
branch = " ╰── "
|
||||||
|
}
|
||||||
|
out.WriteString(tui.Faint.Render(branch) + op.Format() + "\n")
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *removeImageGroupPlan) String() string {
|
||||||
|
return fmt.Sprintf("removeImageGroupPlan[image=%s]", o.Image)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeImageOperation removes an image name or an unnamed image by ID on a machine.
|
||||||
|
type removeImageOperation struct {
|
||||||
|
MachineName string
|
||||||
|
ImageID string
|
||||||
|
// Image is the exact image name or ID sent to Docker.
|
||||||
|
Image string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *removeImageOperation) Execute(ctx context.Context, cli api.ImageClient, untagged map[string]bool) error {
|
||||||
|
if untagged[o.MachineName+"\x00"+o.Image] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
results, err := cli.RemoveImage(ctx, o.Image, api.RemoveImageOptions{Machines: []string{o.MachineName}})
|
||||||
|
// A removal may also untag other planned names. Track actual results across all groups, per machine.
|
||||||
|
for _, result := range results {
|
||||||
|
for _, removal := range result.Results {
|
||||||
|
if removal.Untagged != "" {
|
||||||
|
untagged[result.Metadata.MachineName+"\x00"+removal.Untagged] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if errors.Is(err, api.ErrNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("remove image '%s': %w", o.Image, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *removeImageOperation) Format() string {
|
||||||
|
return tui.BoldRed.Render("-") + " " + tui.FormatImage(o.Image, tui.NoStyle) +
|
||||||
|
tui.Faint.Render(" ("+stringid.TruncateID(o.ImageID)+") on ") + o.MachineName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *removeImageOperation) String() string {
|
||||||
|
return fmt.Sprintf("removeImageOperation[machine_name=%s image=%s]", o.MachineName, o.Image)
|
||||||
|
}
|
||||||
|
|
||||||
|
type machineImageInventory struct {
|
||||||
|
machineName string
|
||||||
|
images []image.Summary
|
||||||
|
}
|
||||||
|
|
||||||
|
// planImageRemoval builds a removal plan grouped by input argument from images on the selected machines.
|
||||||
|
// An empty machines list selects all machines.
|
||||||
|
//
|
||||||
|
// Arguments expand as follows:
|
||||||
|
// - Name: without a tag or digest, select all stored names in that exact repository across image IDs.
|
||||||
|
// An explicit tag selects only that tag. Familiar names are normalised.
|
||||||
|
// - ID: select all RepoTags for the matching image, or its full ID if unnamed. A prefix must identify
|
||||||
|
// one unique full ID across the selected machines.
|
||||||
|
// - Pattern: match RepoTags only. A matching digest also selects that image's names in the same repository.
|
||||||
|
// - Digest: resolve through RepoTags, RepoDigests, or the image ID, then select that image's names in
|
||||||
|
// the same repository. If none exist but RepoDigests contains the reference, select the digest itself.
|
||||||
|
//
|
||||||
|
// Selecting all tags for a repository/image on a machine also adds its stored digest references after
|
||||||
|
// the tags, even if those digests did not match an argument. Stored names are kept as is, and overlapping
|
||||||
|
// selections produce only one operation per machine and name.
|
||||||
|
//
|
||||||
|
// Failed image listings produce warnings and exclude those machines from the plan. If no machine can
|
||||||
|
// be queried, planning fails. Every argument must match on a successfully queried machine. Unmatched
|
||||||
|
// or invalid arguments and ambiguous ID prefixes reject the whole plan before any removal.
|
||||||
|
//
|
||||||
|
// Planning does not check container usage. The backend checks each removal during execution, and failures
|
||||||
|
// do not stop other operations. Tag removal may implicitly untag a used digest. Execution skips names
|
||||||
|
// already untagged by earlier operations and tolerates names that disappeared after planning.
|
||||||
|
func planImageRemoval(ctx context.Context, cli api.ImageClient, images, machines []string) (*imageRemovalPlan, error) {
|
||||||
|
if len(images) == 0 {
|
||||||
|
return nil, errors.New("at least one image is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var clusterImages []api.MachineImages
|
||||||
|
err := tui.RunSpinner(ctx, "Finding images to remove...", func(ctx context.Context) error {
|
||||||
|
var err error
|
||||||
|
clusterImages, err = cli.ListImages(ctx, api.ImageFilter{Machines: machines})
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list images: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var inventory []*machineImageInventory
|
||||||
|
for _, machineImages := range clusterImages {
|
||||||
|
if err := machineImages.Metadata.Err(); err != nil {
|
||||||
|
tui.PrintWarning(fmt.Sprintf("failed to list images on machine '%s': %s",
|
||||||
|
machineImages.Metadata.MachineName, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
machine := &machineImageInventory{
|
||||||
|
machineName: machineImages.Metadata.MachineName,
|
||||||
|
images: machineImages.Images,
|
||||||
|
}
|
||||||
|
inventory = append(inventory, machine)
|
||||||
|
}
|
||||||
|
if len(inventory) == 0 {
|
||||||
|
return nil, errors.New("failed to list images on any machine")
|
||||||
|
}
|
||||||
|
|
||||||
|
return planImageRemovalFromInventory(images, inventory)
|
||||||
|
}
|
||||||
|
|
||||||
|
func planImageRemovalFromInventory(images []string, inventory []*machineImageInventory) (*imageRemovalPlan, error) {
|
||||||
|
plan := &imageRemovalPlan{}
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var errs []error
|
||||||
|
for _, imageArg := range images {
|
||||||
|
group := &removeImageGroupPlan{Image: imageArg}
|
||||||
|
isID := imageIDRegex.MatchString(imageArg)
|
||||||
|
isPattern := strings.ContainsAny(imageArg, "*?[")
|
||||||
|
var namedRef reference.Named
|
||||||
|
if isPattern {
|
||||||
|
if _, err := path.Match(imageArg, ""); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid image pattern '%s': %w", imageArg, err)
|
||||||
|
}
|
||||||
|
} else if !isID {
|
||||||
|
var err error
|
||||||
|
namedRef, err = reference.ParseNormalizedNamed(imageArg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid image '%s': %w", imageArg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isID {
|
||||||
|
matches := make(map[string]bool)
|
||||||
|
for _, machine := range inventory {
|
||||||
|
for _, img := range machine.images {
|
||||||
|
if imageIDMatches(img.ID, imageArg) {
|
||||||
|
matches[img.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(matches) > 1 {
|
||||||
|
var candidates []string
|
||||||
|
for id := range matches {
|
||||||
|
candidates = append(candidates, id)
|
||||||
|
}
|
||||||
|
slices.Sort(candidates)
|
||||||
|
errs = append(errs,
|
||||||
|
fmt.Errorf("image ID '%s' is ambiguous: %s", imageArg, strings.Join(candidates, ", ")))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matched := false
|
||||||
|
for _, machine := range inventory {
|
||||||
|
for _, img := range machine.images {
|
||||||
|
var imagesToRemove []string
|
||||||
|
switch {
|
||||||
|
case isID:
|
||||||
|
if !imageIDMatches(img.ID, imageArg) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
matched = true
|
||||||
|
// Expand only stored names, matching uc image ls. RepoDigests remains available for digest lookup.
|
||||||
|
imagesToRemove = img.RepoTags
|
||||||
|
if len(imagesToRemove) == 0 {
|
||||||
|
imagesToRemove = []string{img.ID}
|
||||||
|
}
|
||||||
|
case isPattern:
|
||||||
|
for _, ref := range img.RepoTags {
|
||||||
|
if ok, _ := path.Match(imageArg, ref); ok {
|
||||||
|
named, _ := reference.ParseDockerRef(ref)
|
||||||
|
if _, digest := named.(reference.Canonical); digest {
|
||||||
|
imagesToRemove = append(imagesToRemove,
|
||||||
|
imageRepositoryReferences(img.RepoTags, named.Name())...)
|
||||||
|
} else {
|
||||||
|
imagesToRemove = append(imagesToRemove, ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reference.IsNameOnly(namedRef):
|
||||||
|
imagesToRemove = imageRepositoryReferences(img.RepoTags, namedRef.Name())
|
||||||
|
default:
|
||||||
|
if canonical, digest := namedRef.(reference.Canonical); digest {
|
||||||
|
ref := reference.FamiliarString(namedRef)
|
||||||
|
if canonical.Digest().String() != img.ID && !slices.Contains(img.RepoTags, ref) &&
|
||||||
|
!slices.Contains(img.RepoDigests, ref) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
imagesToRemove = imageRepositoryReferences(img.RepoTags, namedRef.Name())
|
||||||
|
if len(imagesToRemove) == 0 && slices.Contains(img.RepoDigests, ref) {
|
||||||
|
// An explicitly requested digest can resolve even when no stored name is listed.
|
||||||
|
imagesToRemove = []string{ref}
|
||||||
|
}
|
||||||
|
} else if slices.Contains(img.RepoTags, reference.FamiliarString(namedRef)) {
|
||||||
|
imagesToRemove = []string{reference.FamiliarString(namedRef)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ref := range imagesToRemove {
|
||||||
|
matched = true
|
||||||
|
key := machine.machineName + "\x00" + ref
|
||||||
|
if !seen[key] {
|
||||||
|
seen[key] = true
|
||||||
|
group.Operations = append(group.Operations, &removeImageOperation{
|
||||||
|
MachineName: machine.machineName, ImageID: img.ID, Image: ref,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
errs = append(errs, fmt.Errorf("image '%s' not found on any machine", imageArg))
|
||||||
|
}
|
||||||
|
slices.SortFunc(group.Operations, compareImageRemovalOperations)
|
||||||
|
if len(group.Operations) > 0 {
|
||||||
|
plan.GroupPlans = append(plan.GroupPlans, group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
planDigestRemovals(plan, inventory)
|
||||||
|
if err := errors.Join(errs...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageRepositoryReferences(refs []string, repository string) []string {
|
||||||
|
var matches []string
|
||||||
|
for _, ref := range refs {
|
||||||
|
if named, err := reference.ParseDockerRef(ref); err == nil && named.Name() == repository {
|
||||||
|
matches = append(matches, ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
// planDigestRemovals includes stored digests with the group removing their last repository tag.
|
||||||
|
// RepoDigests also contains synthesised references, so only digest names stored in RepoTags get operations here.
|
||||||
|
func planDigestRemovals(plan *imageRemovalPlan, inventory []*machineImageInventory) {
|
||||||
|
type repository struct {
|
||||||
|
remainingTags int
|
||||||
|
digests []string
|
||||||
|
}
|
||||||
|
repositories := make(map[string]*repository)
|
||||||
|
for _, machine := range inventory {
|
||||||
|
for _, img := range machine.images {
|
||||||
|
names := slices.Clone(img.RepoTags)
|
||||||
|
slices.Sort(names)
|
||||||
|
for _, ref := range slices.Compact(names) {
|
||||||
|
named, err := reference.ParseDockerRef(ref)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := machine.machineName + "\x00" + img.ID + "\x00" + named.Name()
|
||||||
|
repo := repositories[key]
|
||||||
|
if repo == nil {
|
||||||
|
repo = &repository{}
|
||||||
|
repositories[key] = repo
|
||||||
|
}
|
||||||
|
if _, tagged := named.(reference.Tagged); tagged {
|
||||||
|
repo.remainingTags++
|
||||||
|
} else if _, digest := named.(reference.Canonical); digest {
|
||||||
|
repo.digests = append(repo.digests, ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emitted := make(map[string]bool)
|
||||||
|
var groups []*removeImageGroupPlan
|
||||||
|
for _, group := range plan.GroupPlans {
|
||||||
|
var operations []*removeImageOperation
|
||||||
|
for _, op := range group.Operations {
|
||||||
|
key := op.MachineName + "\x00" + op.Image
|
||||||
|
if emitted[key] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
emitted[key] = true
|
||||||
|
operations = append(operations, op)
|
||||||
|
named, err := reference.ParseDockerRef(op.Image)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, tagged := named.(reference.Tagged); !tagged {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
repo := repositories[op.MachineName+"\x00"+op.ImageID+"\x00"+named.Name()]
|
||||||
|
if repo == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
repo.remainingTags--
|
||||||
|
if repo.remainingTags != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, digest := range repo.digests {
|
||||||
|
key := op.MachineName + "\x00" + digest
|
||||||
|
if !emitted[key] {
|
||||||
|
emitted[key] = true
|
||||||
|
operations = append(operations, &removeImageOperation{
|
||||||
|
MachineName: op.MachineName, ImageID: op.ImageID, Image: digest,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group.Operations = operations
|
||||||
|
slices.SortFunc(group.Operations, compareImageRemovalOperations)
|
||||||
|
if len(group.Operations) > 0 {
|
||||||
|
groups = append(groups, group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plan.GroupPlans = groups
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareImageRemovalOperations(a, b *removeImageOperation) int {
|
||||||
|
aDigest, bDigest := strings.Contains(a.Image, "@"), strings.Contains(b.Image, "@")
|
||||||
|
if aDigest != bDigest {
|
||||||
|
if aDigest {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if c := strings.Compare(a.Image, b.Image); c != 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return strings.Compare(a.MachineName, b.MachineName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageIDMatches(id, prefix string) bool {
|
||||||
|
return strings.HasPrefix(strings.TrimPrefix(id, "sha256:"), strings.TrimPrefix(prefix, "sha256:"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPlanImageRemovalFromInventory_Selectors(t *testing.T) {
|
||||||
|
idA, idB := testImageID("a"), testImageID("b")
|
||||||
|
digestRef := "myapp@" + testImageID("c")
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
selector string
|
||||||
|
inventory []*machineImageInventory
|
||||||
|
want []removeImageOperation
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "repository selects all stored names across images and machines", selector: "myapp",
|
||||||
|
inventory: []*machineImageInventory{
|
||||||
|
testMachineInventory("dev1", []image.Summary{
|
||||||
|
testImage(idA, "myapp:old", "other:keep"),
|
||||||
|
testImage(idB, "myapp:latest"),
|
||||||
|
testImage(testImageID("c"), digestRef),
|
||||||
|
testImage(testImageID("d"), "myapp-extra:latest", "team/myapp:latest", "myapp/worker:latest"),
|
||||||
|
}),
|
||||||
|
testMachineInventory("machine2", []image.Summary{testImage(idB, "myapp:test")}),
|
||||||
|
},
|
||||||
|
want: []removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: idB, Image: "myapp:latest"},
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:old"},
|
||||||
|
{MachineName: "machine2", ImageID: idB, Image: "myapp:test"},
|
||||||
|
{MachineName: "dev1", ImageID: testImageID("c"), Image: digestRef},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "repository without latest includes its tags and stored digest", selector: "myapp",
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{
|
||||||
|
testImage(idA, "myapp:old", digestRef, "other:keep"),
|
||||||
|
})},
|
||||||
|
want: []removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:old"},
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: digestRef},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "explicit latest selects only that tag", selector: "myapp:latest",
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{
|
||||||
|
testImage(idA, "myapp:old", "myapp:latest", digestRef),
|
||||||
|
})},
|
||||||
|
want: []removeImageOperation{{MachineName: "dev1", ImageID: idA, Image: "myapp:latest"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "qualified repository selects familiar names", selector: "docker.io/library/myapp",
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{
|
||||||
|
testImage(idA, "myapp:old", "myapp:latest", "other:keep"),
|
||||||
|
})},
|
||||||
|
want: []removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:latest"},
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:old"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "registry port does not count as an explicit tag", selector: "registry.example.com:5000/team/myapp",
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{
|
||||||
|
testImage(idA, "registry.example.com:5000/team/myapp:old", "registry.example.com:5000/team/myapp:new",
|
||||||
|
"registry.example.com:5001/team/myapp:keep", "registry.example.com:5000/team/myapp/worker:keep"),
|
||||||
|
})},
|
||||||
|
want: []removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "registry.example.com:5000/team/myapp:new"},
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "registry.example.com:5000/team/myapp:old"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "explicit tag with registry port stays exact", selector: "registry.example.com:5000/team/myapp:old",
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{
|
||||||
|
testImage(idA, "registry.example.com:5000/team/myapp:old", "registry.example.com:5000/team/myapp:new"),
|
||||||
|
})},
|
||||||
|
want: []removeImageOperation{{MachineName: "dev1", ImageID: idA, Image: "registry.example.com:5000/team/myapp:old"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag uses each machines local image and allows missing copies", selector: "myapp:old",
|
||||||
|
inventory: []*machineImageInventory{
|
||||||
|
testMachineInventory("machine2", []image.Summary{testImage(idB, "myapp:old")}),
|
||||||
|
testMachineInventory("empty", nil),
|
||||||
|
testMachineInventory("dev1", []image.Summary{testImage(idA, "myapp:old")}),
|
||||||
|
},
|
||||||
|
want: []removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:old"},
|
||||||
|
{MachineName: "machine2", ImageID: idB, Image: "myapp:old"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "qualified names use familiar references", selector: "docker.io/library/myapp:old",
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{
|
||||||
|
testImage(idA, "myapp:old"),
|
||||||
|
})},
|
||||||
|
want: []removeImageOperation{{MachineName: "dev1", ImageID: idA, Image: "myapp:old"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "full ID expands all repositories and deduplicates references per machine", selector: idA,
|
||||||
|
inventory: []*machineImageInventory{
|
||||||
|
testMachineInventory("dev1", []image.Summary{
|
||||||
|
{
|
||||||
|
ID: idA,
|
||||||
|
RepoTags: []string{"other:keep", "myapp:old", "myapp:old", digestRef, "other@" + testImageID("c")},
|
||||||
|
RepoDigests: []string{
|
||||||
|
digestRef, "other@" + testImageID("c"), "other@" + testImageID("c"), testImageID("c"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testImage(idB, "unrelated:keep"),
|
||||||
|
}),
|
||||||
|
testMachineInventory("machine2", []image.Summary{{
|
||||||
|
ID: idA, RepoTags: []string{"myapp:old", digestRef}, RepoDigests: []string{digestRef},
|
||||||
|
}}),
|
||||||
|
},
|
||||||
|
want: []removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:old"},
|
||||||
|
{MachineName: "machine2", ImageID: idA, Image: "myapp:old"},
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "other:keep"},
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: digestRef},
|
||||||
|
{MachineName: "machine2", ImageID: idA, Image: digestRef},
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "other@" + testImageID("c")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same full ID on multiple machines is unambiguous", selector: "aaaaaaaa",
|
||||||
|
inventory: []*machineImageInventory{
|
||||||
|
testMachineInventory("dev1", []image.Summary{testImage(idA, "myapp:old")}),
|
||||||
|
testMachineInventory("dev1", []image.Summary{testImage(idB, "myapp:latest")}),
|
||||||
|
testMachineInventory("machine2", []image.Summary{testImage(idA, "other:test")}),
|
||||||
|
},
|
||||||
|
want: []removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:old"},
|
||||||
|
{MachineName: "machine2", ImageID: idA, Image: "other:test"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "short ID with algorithm prefix", selector: "sha256:aaaaaaaa",
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1",
|
||||||
|
[]image.Summary{testImage(idA, "myapp:old")})},
|
||||||
|
want: []removeImageOperation{{MachineName: "dev1", ImageID: idA, Image: "myapp:old"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unnamed image is removed by its full ID", selector: idA,
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{{ID: idA}})},
|
||||||
|
want: []removeImageOperation{{MachineName: "dev1", ImageID: idA, Image: idA}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ID expands only stored names", selector: idA,
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{{
|
||||||
|
ID: idA, RepoTags: []string{"myapp:latest"},
|
||||||
|
RepoDigests: []string{"myapp@" + idA, "other@" + idA},
|
||||||
|
}})},
|
||||||
|
want: []removeImageOperation{{MachineName: "dev1", ImageID: idA, Image: "myapp:latest"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ID removes an untagged image by ID even if RepoDigests is populated", selector: idA,
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{{
|
||||||
|
ID: idA, RepoDigests: []string{digestRef},
|
||||||
|
}})},
|
||||||
|
want: []removeImageOperation{{MachineName: "dev1", ImageID: idA, Image: idA}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "synthesized digest selects repository tags without a digest row", selector: digestRef,
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{{
|
||||||
|
ID: idA, RepoTags: []string{"myapp:old", "other:keep", "myapp:latest"},
|
||||||
|
RepoDigests: []string{digestRef, "other@" + testImageID("c")},
|
||||||
|
}})},
|
||||||
|
want: []removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:latest"},
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:old"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "explicit digest lookup works without stored names", selector: digestRef,
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{{
|
||||||
|
ID: idA, RepoDigests: []string{digestRef},
|
||||||
|
}})},
|
||||||
|
want: []removeImageOperation{{MachineName: "dev1", ImageID: idA, Image: digestRef}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "synthesized digest resolves by target and repository even if not listed", selector: "myapp@" + idA,
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{{
|
||||||
|
ID: idA, RepoTags: []string{"myapp:old", "other:keep"}, RepoDigests: []string{"other@" + idA},
|
||||||
|
}})},
|
||||||
|
want: []removeImageOperation{{MachineName: "dev1", ImageID: idA, Image: "myapp:old"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stored digest follows its repository tags", selector: digestRef,
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{
|
||||||
|
testImage(idA, "myapp:old", "myapp:latest", digestRef, "other:keep"),
|
||||||
|
testImage(idB, "myapp@"+idB),
|
||||||
|
})},
|
||||||
|
want: []removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:latest"},
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:old"},
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: digestRef},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "digest pattern also expands matching repository tags", selector: "myapp@sha256:c*",
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{
|
||||||
|
testImage(idA, "myapp:old", "myapp:latest", digestRef, "other:keep"),
|
||||||
|
testImage(idB, "myapp@"+idB),
|
||||||
|
})},
|
||||||
|
want: []removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:latest"},
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:old"},
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: digestRef},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "digest pattern matches stored names and ignores derived names", selector: "myapp@sha256:*",
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{
|
||||||
|
testImage(idA, "myapp:old", digestRef),
|
||||||
|
{ID: idB, RepoTags: []string{"myapp:latest"}, RepoDigests: []string{"myapp@" + idB}},
|
||||||
|
})},
|
||||||
|
want: []removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:old"},
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: digestRef},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "last repository tag includes its stored digest", selector: "myapp:old",
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{
|
||||||
|
testImage(idA, "myapp:old", digestRef, "other:keep"),
|
||||||
|
})},
|
||||||
|
want: []removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:old"},
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: digestRef},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "last tag does not add a synthesized digest row", selector: "myapp:old",
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{{
|
||||||
|
ID: idA, RepoTags: []string{"myapp:old", "other:keep"}, RepoDigests: []string{digestRef},
|
||||||
|
}})},
|
||||||
|
want: []removeImageOperation{{MachineName: "dev1", ImageID: idA, Image: "myapp:old"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remaining repository tag preserves its stored digest", selector: "myapp:old",
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{
|
||||||
|
testImage(idA, "myapp:old", "myapp:latest", digestRef),
|
||||||
|
})},
|
||||||
|
want: []removeImageOperation{{MachineName: "dev1", ImageID: idA, Image: "myapp:old"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "digest without repository tags remains executable", selector: digestRef,
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{
|
||||||
|
testImage(idA, digestRef, "other:keep"),
|
||||||
|
})},
|
||||||
|
want: []removeImageOperation{{MachineName: "dev1", ImageID: idA, Image: digestRef}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "digest-only image keeps its named reference", selector: idA,
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{{
|
||||||
|
ID: idA, RepoTags: []string{digestRef}, RepoDigests: []string{digestRef, testImageID("c")},
|
||||||
|
}})},
|
||||||
|
want: []removeImageOperation{{MachineName: "dev1", ImageID: idA, Image: digestRef}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag pattern selects only matching tags", selector: "myapp:1.[12]",
|
||||||
|
inventory: []*machineImageInventory{testMachineInventory("dev1", []image.Summary{{
|
||||||
|
ID: idA, RepoTags: []string{"myapp:1.3", "myapp:1.2", "myapp:1.1"}, RepoDigests: []string{digestRef},
|
||||||
|
}})},
|
||||||
|
want: []removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:1.1"},
|
||||||
|
{MachineName: "dev1", ImageID: idA, Image: "myapp:1.2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
plan, err := planImageRemovalFromInventory([]string{tt.selector}, tt.inventory)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, plan.GroupPlans, 1)
|
||||||
|
assert.Equal(t, tt.selector, plan.GroupPlans[0].Image)
|
||||||
|
|
||||||
|
var got []removeImageOperation
|
||||||
|
for _, op := range plan.GroupPlans[0].Operations {
|
||||||
|
got = append(got, *op)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanImageRemovalFromInventory_OverlappingSelectors(t *testing.T) {
|
||||||
|
inventory := []*machineImageInventory{
|
||||||
|
testMachineInventory("dev1", []image.Summary{testImage(testImageID("a"), "myapp:old", "myapp:latest")}),
|
||||||
|
testMachineInventory("machine2", []image.Summary{testImage(testImageID("b"), "myapp:test", "myapp:old")}),
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := planImageRemovalFromInventory([]string{"myapp:old", "myapp", "myapp:*", testImageID("a"), "myapp:old"},
|
||||||
|
inventory)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, &imageRemovalPlan{GroupPlans: []*removeImageGroupPlan{
|
||||||
|
{Image: "myapp:old", Operations: []*removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: testImageID("a"), Image: "myapp:old"},
|
||||||
|
{MachineName: "machine2", ImageID: testImageID("b"), Image: "myapp:old"},
|
||||||
|
}},
|
||||||
|
{Image: "myapp", Operations: []*removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: testImageID("a"), Image: "myapp:latest"},
|
||||||
|
{MachineName: "machine2", ImageID: testImageID("b"), Image: "myapp:test"},
|
||||||
|
}},
|
||||||
|
}}, plan, "the first selector owns each removal and empty groups are omitted")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanImageRemovalFromInventory_DigestFollowsLastTagAcrossSelectors(t *testing.T) {
|
||||||
|
imageID := testImageID("a")
|
||||||
|
digestRef := "myapp@" + imageID
|
||||||
|
inventory := []*machineImageInventory{testMachineInventory("dev1", []image.Summary{
|
||||||
|
testImage(imageID, "myapp:old", "myapp:latest", "myapp:old", digestRef, digestRef, "other:keep"),
|
||||||
|
})}
|
||||||
|
|
||||||
|
plan, err := planImageRemovalFromInventory([]string{"myapp:old", "myapp:latest", digestRef}, inventory)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, &imageRemovalPlan{GroupPlans: []*removeImageGroupPlan{
|
||||||
|
{Image: "myapp:old", Operations: []*removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: imageID, Image: "myapp:old"},
|
||||||
|
}},
|
||||||
|
{Image: "myapp:latest", Operations: []*removeImageOperation{
|
||||||
|
{MachineName: "dev1", ImageID: imageID, Image: "myapp:latest"},
|
||||||
|
{MachineName: "dev1", ImageID: imageID, Image: digestRef},
|
||||||
|
}},
|
||||||
|
}}, plan, "digest cleanup belongs to the last tag removal, so the later digest selector has no extra operation")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanImageRemovalFromInventory_RejectsInvalidOrMissingSelectors(t *testing.T) {
|
||||||
|
idA := "sha256:abcdef" + strings.Repeat("a", 58)
|
||||||
|
idB := "sha256:abcdef" + strings.Repeat("b", 58)
|
||||||
|
inventory := []*machineImageInventory{
|
||||||
|
testMachineInventory("dev1", []image.Summary{{
|
||||||
|
ID: idA, RepoTags: []string{"myapp:old"}, RepoDigests: []string{"myapp@" + idA, "derived@" + idA},
|
||||||
|
}}),
|
||||||
|
testMachineInventory("machine2", []image.Summary{{ID: idB, RepoTags: []string{"myapp:old"}}}),
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
images []string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{"missing repository", []string{"missing"}, "not found"},
|
||||||
|
{"repository only has a derived digest", []string{"derived"}, "not found"},
|
||||||
|
{"missing tag", []string{"myapp:missing"}, "not found"},
|
||||||
|
{"missing pattern", []string{"other:*"}, "not found"},
|
||||||
|
{"pattern only matches a derived digest", []string{"myapp@sha256:*"}, "not found"},
|
||||||
|
{"missing ID", []string{testImageID("c")}, "not found"},
|
||||||
|
{"ambiguous ID prefix", []string{"abcdef"}, "ambiguous"},
|
||||||
|
{"invalid name", []string{"MyApp:old"}, "invalid image"},
|
||||||
|
{"invalid pattern", []string{"myapp:["}, "invalid image pattern"},
|
||||||
|
{"one missing selector rejects the whole plan", []string{"myapp:old", "myapp:missing"}, "not found"},
|
||||||
|
{"one invalid selector rejects the whole plan", []string{"myapp:old", "myapp:["}, "invalid image pattern"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
plan, err := planImageRemovalFromInventory(tt.images, inventory)
|
||||||
|
require.ErrorContains(t, err, tt.err)
|
||||||
|
assert.Nil(t, plan, "a failed selector must not return a partial plan")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("ID prefix resolves within the supplied machines", func(t *testing.T) {
|
||||||
|
plan, err := planImageRemovalFromInventory([]string{"abcdef"}, inventory[:1])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, plan.GroupPlans, 1)
|
||||||
|
require.Len(t, plan.GroupPlans[0].Operations, 1)
|
||||||
|
assert.Equal(t, idA, plan.GroupPlans[0].Operations[0].ImageID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanImageRemovalFromInventory_InUseImagesAreStillPlanned(t *testing.T) {
|
||||||
|
img := testImage(testImageID("a"), "myapp:old", "myapp:current")
|
||||||
|
img.Containers = 1
|
||||||
|
|
||||||
|
for _, selector := range []string{"myapp", "myapp:current", img.ID, "myapp:*"} {
|
||||||
|
t.Run(selector, func(t *testing.T) {
|
||||||
|
plan, err := planImageRemovalFromInventory([]string{selector}, []*machineImageInventory{
|
||||||
|
testMachineInventory("dev1", []image.Summary{img}),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, plan.IsEmpty(), "usage protection belongs to the backend at execution time")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testImageID(char string) string { return "sha256:" + strings.Repeat(char, 64) }
|
||||||
|
|
||||||
|
func testImage(imageID string, tags ...string) image.Summary {
|
||||||
|
return image.Summary{ID: imageID, RepoTags: tags}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMachineInventory(name string, images []image.Summary) *machineImageInventory {
|
||||||
|
return &machineImageInventory{machineName: name, images: images}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ func NewRootCommand() *cobra.Command {
|
|||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
NewListCommand(),
|
NewListCommand(),
|
||||||
NewPushCommand(),
|
NewPushCommand(),
|
||||||
|
NewRemoveCommand(),
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|||||||
+4
-22
@@ -187,14 +187,12 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
|
|||||||
}
|
}
|
||||||
fmt.Println("Machine joined the cluster.")
|
fmt.Println("Machine joined the cluster.")
|
||||||
|
|
||||||
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
|
// Deploy the current caddy service spec to run a container on the added machine. This keeps the service
|
||||||
// that may cause a small downtime.
|
// configuration however it was deployed and shouldn't change the containers on other machines if specs on them
|
||||||
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
|
// haven't diverged.
|
||||||
// use the deployed image version.
|
|
||||||
// NOTE: We use the cluster client to inspect and scale the Caddy service because the newly added machine may have
|
// NOTE: We use the cluster client to inspect and scale the Caddy service because the newly added machine may have
|
||||||
// issues accessing the API of existing machines in the cluster.
|
// issues accessing the API of existing machines in the cluster.
|
||||||
// See the issue for more details: https://github.com/psviderski/uncloud/issues/65.
|
// See the issue for more details: https://github.com/psviderski/uncloud/issues/65.
|
||||||
caddyImage := ""
|
|
||||||
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, api.ErrNotFound) {
|
if errors.Is(err, api.ErrNotFound) {
|
||||||
@@ -203,26 +201,10 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
|
|||||||
}
|
}
|
||||||
return fmt.Errorf("inspect caddy service: %w", err)
|
return fmt.Errorf("inspect caddy service: %w", err)
|
||||||
}
|
}
|
||||||
caddyImage = caddySvc.Containers[0].Container.Config.Image
|
|
||||||
// Find the latest created container and use its image.
|
|
||||||
var latestCreated time.Time
|
|
||||||
for _, c := range caddySvc.Containers[1:] {
|
|
||||||
created, err := time.Parse(time.RFC3339Nano, c.Container.Created)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if created.After(latestCreated) {
|
|
||||||
latestCreated = created
|
|
||||||
caddyImage = c.Container.Config.Image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("Preparing Caddy deployment...")
|
fmt.Println("Preparing Caddy deployment...")
|
||||||
d, err := clusterClient.NewCaddyDeployment(caddyImage, "", api.Placement{})
|
d := clusterClient.NewDeployment(caddySvc.Spec(), nil)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create caddy deployment: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
plan, err := d.Plan(ctx)
|
plan, err := d.Plan(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"github.com/psviderski/uncloud/internal/cli/tui"
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
"github.com/psviderski/uncloud/internal/machine/cluster"
|
"github.com/psviderski/uncloud/internal/machine/cluster"
|
||||||
"github.com/psviderski/uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -221,17 +221,17 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
|||||||
initOpts.WireguardEndpoints = endpoints
|
initOpts.WireguardEndpoints = endpoints
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := uncli.InitCluster(ctx, initOpts)
|
clusterClient, err := uncli.InitCluster(ctx, initOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer clusterClient.Close()
|
||||||
|
|
||||||
// Since the cluster API needs a few moments to become ready after cluster initialisation,
|
// Since the cluster API needs a few moments to become ready after cluster initialisation,
|
||||||
// we keep the user informed during this wait. We wait here even if no Caddy or DNS is requested
|
// we keep the user informed during this wait. We wait here even if no Caddy or DNS is requested
|
||||||
// as the cluster needs to be ready so that commands such as 'uc machine ls' work immediately after init.
|
// as the cluster needs to be ready so that commands such as 'uc machine ls' work immediately after init.
|
||||||
err = tui.RunSpinner(ctx, "Waiting for the cluster to be ready...", func(ctx context.Context) error {
|
err = tui.RunSpinner(ctx, "Waiting for the cluster to be ready...", func(ctx context.Context) error {
|
||||||
return client.WaitClusterReady(ctx, 1*time.Minute)
|
return clusterClient.WaitClusterReady(ctx, 1*time.Minute)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wait for cluster to be ready: %w", err)
|
return fmt.Errorf("wait for cluster to be ready: %w", err)
|
||||||
@@ -245,7 +245,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
if !opts.noDNS {
|
if !opts.noDNS {
|
||||||
domain, err := client.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.dnsEndpoint})
|
domain, err := clusterClient.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.dnsEndpoint})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("reserve cluster domain in Uncloud DNS: %w", err)
|
return fmt.Errorf("reserve cluster domain in Uncloud DNS: %w", err)
|
||||||
}
|
}
|
||||||
@@ -253,7 +253,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !opts.noCaddy {
|
if !opts.noCaddy {
|
||||||
d, err := client.NewCaddyDeployment("", "", api.Placement{})
|
d, err := clusterClient.Caddy.NewDeployment(ctx, client.CaddyDeploymentOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create caddy deployment: %w", err)
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
}
|
}
|
||||||
@@ -269,7 +269,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
return caddy.UpdateDomainRecords(ctx, client, uncli.ProgressOut())
|
return caddy.UpdateDomainRecords(ctx, clusterClient, uncli.ProgressOut())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+12
-7
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/internal/cli/completion"
|
"github.com/psviderski/uncloud/internal/cli/completion"
|
||||||
@@ -64,6 +65,15 @@ If no services are specified, streams logs from the uncloud service.`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runLogs(ctx context.Context, uncli *cli.CLI, services []string, opts logs.Options) error {
|
func runLogs(ctx context.Context, uncli *cli.CLI, services []string, opts logs.Options) error {
|
||||||
|
since, until, err := logs.TimeRange(opts.Since, opts.Until, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tail, err := opts.TailLines()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if len(services) == 0 {
|
if len(services) == 0 {
|
||||||
services = []string{api.SystemServiceUncloud}
|
services = []string{api.SystemServiceUncloud}
|
||||||
}
|
}
|
||||||
@@ -74,11 +84,6 @@ func runLogs(ctx context.Context, uncli *cli.CLI, services []string, opts logs.O
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tail, err := logs.Tail(opts.Tail)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := uncli.ConnectCluster(ctx)
|
c, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
@@ -88,8 +93,8 @@ func runLogs(ctx context.Context, uncli *cli.CLI, services []string, opts logs.O
|
|||||||
logsOpts := api.ServiceLogsOptions{
|
logsOpts := api.ServiceLogsOptions{
|
||||||
Follow: opts.Follow,
|
Follow: opts.Follow,
|
||||||
Tail: tail,
|
Tail: tail,
|
||||||
Since: opts.Since,
|
Since: since,
|
||||||
Until: opts.Until,
|
Until: until,
|
||||||
Machines: cli.ExpandCommaSeparatedValues(opts.Machines),
|
Machines: cli.ExpandCommaSeparatedValues(opts.Machines),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/logs"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunLogsInvalidTimeFilters(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Invalid filters must fail before connecting to the cluster.
|
||||||
|
for _, flag := range []string{"since", "until"} {
|
||||||
|
opts := logs.Options{}
|
||||||
|
if flag == "since" {
|
||||||
|
opts.Since = "invalid"
|
||||||
|
} else {
|
||||||
|
opts.Until = "invalid"
|
||||||
|
}
|
||||||
|
err := runLogs(context.Background(), nil, nil, opts)
|
||||||
|
require.ErrorContains(t, err, "invalid --"+flag+" value")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,8 +54,8 @@ func rtt(ctx context.Context, uncli *cli.CLI) error {
|
|||||||
tui.PrintWarning("metadata is missing in response from unknown server")
|
tui.PrintWarning("metadata is missing in response from unknown server")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if m.Metadata.Error != "" {
|
if err := m.Metadata.Err(); err != nil {
|
||||||
tui.PrintWarning(fmt.Sprintf("failed to inspect machine '%s': %s", m.Metadata.MachineName, m.Metadata.Error))
|
tui.PrintWarning(fmt.Sprintf("failed to inspect machine '%s': %s", m.Metadata.MachineName, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if m.Machine == nil {
|
if m.Machine == nil {
|
||||||
|
|||||||
+2
-2
@@ -207,9 +207,9 @@ func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo
|
|||||||
|
|
||||||
machineName := msc.Metadata.MachineName
|
machineName := msc.Metadata.MachineName
|
||||||
|
|
||||||
if msc.Metadata.Error != "" {
|
if err := msc.Metadata.Err(); err != nil {
|
||||||
tui.PrintWarning(fmt.Sprintf("failed to list service containers on machine %s: %s",
|
tui.PrintWarning(fmt.Sprintf("failed to list service containers on machine %s: %s",
|
||||||
machineName, msc.Metadata.Error))
|
machineName, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -58,6 +59,16 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
|||||||
fmt.Printf("Service ID: %s\n", svc.ID)
|
fmt.Printf("Service ID: %s\n", svc.ID)
|
||||||
fmt.Printf("Name: %s\n", svc.Name)
|
fmt.Printf("Name: %s\n", svc.Name)
|
||||||
fmt.Printf("Mode: %s\n", svc.Mode)
|
fmt.Printf("Mode: %s\n", svc.Mode)
|
||||||
|
|
||||||
|
spec := svc.Spec()
|
||||||
|
if len(spec.Labels) > 0 {
|
||||||
|
fmt.Println("Labels:")
|
||||||
|
printLabels(spec.Labels)
|
||||||
|
}
|
||||||
|
if len(spec.Container.Labels) > 0 {
|
||||||
|
fmt.Println("Container labels:")
|
||||||
|
printLabels(spec.Container.Labels)
|
||||||
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
// Combine regular and hook containers.
|
// Combine regular and hook containers.
|
||||||
@@ -130,3 +141,10 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
|||||||
lipgloss.Println(t)
|
lipgloss.Println(t)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// printLabels prints the labels sorted by key followed by a blank line.
|
||||||
|
func printLabels(labels map[string]string) {
|
||||||
|
for _, key := range slices.Sorted(maps.Keys(labels)) {
|
||||||
|
lipgloss.Println(" " + key + tui.Faint.Render("=") + labels[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+12
-8
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
mapset "github.com/deckarep/golang-set/v2"
|
mapset "github.com/deckarep/golang-set/v2"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
@@ -78,6 +79,15 @@ If no services are specified, streams logs from all services defined in the Comp
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RunLogs(ctx context.Context, uncli *cli.CLI, args []string, opts logs.Options) error {
|
func RunLogs(ctx context.Context, uncli *cli.CLI, args []string, opts logs.Options) error {
|
||||||
|
since, until, err := logs.TimeRange(opts.Since, opts.Until, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tail, err := opts.TailLines()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
serviceArgs, err := logs.ParseServiceArgs(args)
|
serviceArgs, err := logs.ParseServiceArgs(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -106,12 +116,6 @@ func RunLogs(ctx context.Context, uncli *cli.CLI, args []string, opts logs.Optio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse tail option.
|
|
||||||
tail, err := logs.Tail(opts.Tail)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := uncli.ConnectCluster(ctx)
|
c, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
@@ -121,8 +125,8 @@ func RunLogs(ctx context.Context, uncli *cli.CLI, args []string, opts logs.Optio
|
|||||||
baseOpts := api.ServiceLogsOptions{
|
baseOpts := api.ServiceLogsOptions{
|
||||||
Follow: opts.Follow,
|
Follow: opts.Follow,
|
||||||
Tail: tail,
|
Tail: tail,
|
||||||
Since: opts.Since,
|
Since: since,
|
||||||
Until: opts.Until,
|
Until: until,
|
||||||
Machines: cli.ExpandCommaSeparatedValues(opts.Machines),
|
Machines: cli.ExpandCommaSeparatedValues(opts.Machines),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/logs"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunLogsInvalidTimeFilters(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Invalid filters must fail before loading Compose files or connecting to the cluster.
|
||||||
|
for _, flag := range []string{"since", "until"} {
|
||||||
|
opts := logs.Options{}
|
||||||
|
if flag == "since" {
|
||||||
|
opts.Since = "invalid"
|
||||||
|
} else {
|
||||||
|
opts.Until = "invalid"
|
||||||
|
}
|
||||||
|
err := RunLogs(context.Background(), nil, nil, opts)
|
||||||
|
require.ErrorContains(t, err, "invalid --"+flag+" value")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ func NewRootCommand() *cobra.Command {
|
|||||||
NewScaleCommand(""),
|
NewScaleCommand(""),
|
||||||
NewStartCommand(""),
|
NewStartCommand(""),
|
||||||
NewStopCommand(""),
|
NewStopCommand(""),
|
||||||
|
NewUpdateCommand(),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-3
@@ -21,13 +21,16 @@ import (
|
|||||||
type runOptions struct {
|
type runOptions struct {
|
||||||
caddyfile string
|
caddyfile string
|
||||||
command []string
|
command []string
|
||||||
|
containerLabels []string
|
||||||
cpu dockeropts.NanoCPUs
|
cpu dockeropts.NanoCPUs
|
||||||
entrypoint string
|
entrypoint string
|
||||||
entrypointChanged bool
|
entrypointChanged bool
|
||||||
env []string
|
env []string
|
||||||
image string
|
image string
|
||||||
|
labels []string
|
||||||
machines []string
|
machines []string
|
||||||
memory dockeropts.MemBytes
|
memory dockeropts.MemBytes
|
||||||
|
memoryReservation dockeropts.MemBytes
|
||||||
mode string
|
mode string
|
||||||
shmSize dockeropts.MemBytes
|
shmSize dockeropts.MemBytes
|
||||||
name string
|
name string
|
||||||
@@ -64,6 +67,9 @@ func NewRunCommand(groupID string) *cobra.Command {
|
|||||||
cmd.Flags().StringVar(&opts.caddyfile, "caddyfile", "",
|
cmd.Flags().StringVar(&opts.caddyfile, "caddyfile", "",
|
||||||
"Path to a custom Caddy config (Caddyfile) for the service. "+
|
"Path to a custom Caddy config (Caddyfile) for the service. "+
|
||||||
"Cannot be used together with non-@host published ports.")
|
"Cannot be used together with non-@host published ports.")
|
||||||
|
cmd.Flags().StringArrayVar(&opts.containerLabels, "container-label", nil,
|
||||||
|
"Set a Docker label on service containers. Can be specified multiple times.\n"+
|
||||||
|
"Format: key=value or just key for an empty value.")
|
||||||
cmd.Flags().VarP(&opts.cpu, "cpu", "",
|
cmd.Flags().VarP(&opts.cpu, "cpu", "",
|
||||||
"Maximum number of CPU cores a service container can use. Fractional values are allowed: "+
|
"Maximum number of CPU cores a service container can use. Fractional values are allowed: "+
|
||||||
"0.5 for half a core or 2.25 for two and a quarter cores.")
|
"0.5 for half a core or 2.25 for two and a quarter cores.")
|
||||||
@@ -72,6 +78,9 @@ func NewRunCommand(groupID string) *cobra.Command {
|
|||||||
cmd.Flags().StringSliceVarP(&opts.env, "env", "e", nil,
|
cmd.Flags().StringSliceVarP(&opts.env, "env", "e", nil,
|
||||||
"Set an environment variable for service containers. Can be specified multiple times.\n"+
|
"Set an environment variable for service containers. Can be specified multiple times.\n"+
|
||||||
"Format: VAR=value or just VAR to use the value from the local environment.")
|
"Format: VAR=value or just VAR to use the value from the local environment.")
|
||||||
|
cmd.Flags().StringArrayVarP(&opts.labels, "label", "l", nil,
|
||||||
|
"Set a service label. Service labels aren't applied to Docker containers, use --container-label for that.\n"+
|
||||||
|
"Can be specified multiple times. Format: key=value or just key for an empty value.")
|
||||||
cmd.Flags().StringVar(&opts.mode, "mode", api.ServiceModeReplicated,
|
cmd.Flags().StringVar(&opts.mode, "mode", api.ServiceModeReplicated,
|
||||||
fmt.Sprintf("Replication mode of the service: either '%s' (a specified number of containers across "+
|
fmt.Sprintf("Replication mode of the service: either '%s' (a specified number of containers across "+
|
||||||
"the machines) or '%s' (one container on every machine).",
|
"the machines) or '%s' (one container on every machine).",
|
||||||
@@ -83,6 +92,9 @@ func NewRunCommand(groupID string) *cobra.Command {
|
|||||||
"Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix (b, k, m, g).\n"+
|
"Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix (b, k, m, g).\n"+
|
||||||
"Default unit is bytes if no suffix specified.\n"+
|
"Default unit is bytes if no suffix specified.\n"+
|
||||||
"Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)")
|
"Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)")
|
||||||
|
cmd.Flags().Var(&opts.memoryReservation, "memory-reservation",
|
||||||
|
"Minimum amount of memory reserved for a service container. Value is a positive integer with optional unit suffix (b, k, m, g).\n"+
|
||||||
|
"Default unit is bytes if no suffix specified.")
|
||||||
cmd.Flags().VarP(&opts.shmSize, "shm-size", "",
|
cmd.Flags().VarP(&opts.shmSize, "shm-size", "",
|
||||||
"Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer\n"+
|
"Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer\n"+
|
||||||
"with optional unit suffix (b, k, m, g). Default unit is bytes if no suffix specified.\n"+
|
"with optional unit suffix (b, k, m, g). Default unit is bytes if no suffix specified.\n"+
|
||||||
@@ -94,11 +106,12 @@ func NewRunCommand(groupID string) *cobra.Command {
|
|||||||
cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
|
cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
|
||||||
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
|
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
|
||||||
"Format: [hostname:]container_port[/protocol] or [host_ip|host_prefix:]host_port:container_port[/protocol]@host\n"+
|
"Format: [hostname:]container_port[/protocol] or [host_ip|host_prefix:]host_port:container_port[/protocol]@host\n"+
|
||||||
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified\n"+
|
"Supported protocols: tcp, udp, http, https. The protocol defaults to https if a hostname is specified,\n"+
|
||||||
"and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.\n"+
|
"otherwise tcp. http(s) ports without a hostname use 'service-name.cluster-domain' as the default hostname\n"+
|
||||||
|
"if a cluster domain is reserved.\n"+
|
||||||
"Examples:\n"+
|
"Examples:\n"+
|
||||||
" -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname\n"+
|
" -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname\n"+
|
||||||
" -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname\n"+
|
" -p app.example.com:8080 Publish port 8080 as HTTPS via reverse proxy with custom hostname\n"+
|
||||||
// TODO: add support for publishing L4 tcp/udp ports.
|
// TODO: add support for publishing L4 tcp/udp ports.
|
||||||
//" -p 9000:8080 Publish port 8080 as TCP port 9000 via reverse proxy\n"+
|
//" -p 9000:8080 Publish port 8080 as TCP port 9000 via reverse proxy\n"+
|
||||||
" -p 53:5353/udp@host Bind UDP port 5353 to host port 53\n"+
|
" -p 53:5353/udp@host Bind UDP port 5353 to host port 53\n"+
|
||||||
@@ -122,6 +135,7 @@ func NewRunCommand(groupID string) *cobra.Command {
|
|||||||
"Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where\n"+
|
"Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where\n"+
|
||||||
"the volume is located. Can be specified multiple times.\n"+
|
"the volume is located. Can be specified multiple times.\n"+
|
||||||
"Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]\n"+
|
"Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]\n"+
|
||||||
|
"Host and container paths support runtime templates such as {{.Container.Name}}.\n"+
|
||||||
"Examples:\n"+
|
"Examples:\n"+
|
||||||
" -v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container\n"+
|
" -v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container\n"+
|
||||||
" -v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container\n"+
|
" -v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container\n"+
|
||||||
@@ -191,6 +205,18 @@ func prepareServiceSpec(opts runOptions) (api.ServiceSpec, error) {
|
|||||||
return spec, err
|
return spec, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
labels, err := parseLabels(opts.labels)
|
||||||
|
if err != nil {
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
|
if err = api.ValidateLabels(labels); err != nil {
|
||||||
|
return spec, fmt.Errorf("invalid service labels: %w", err)
|
||||||
|
}
|
||||||
|
containerLabels, err := parseLabels(opts.containerLabels)
|
||||||
|
if err != nil {
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
|
|
||||||
switch opts.mode {
|
switch opts.mode {
|
||||||
case api.ServiceModeReplicated, api.ServiceModeGlobal:
|
case api.ServiceModeReplicated, api.ServiceModeGlobal:
|
||||||
default:
|
default:
|
||||||
@@ -231,17 +257,20 @@ func prepareServiceSpec(opts runOptions) (api.ServiceSpec, error) {
|
|||||||
Command: opts.command,
|
Command: opts.command,
|
||||||
Env: env,
|
Env: env,
|
||||||
Image: opts.image,
|
Image: opts.image,
|
||||||
|
Labels: containerLabels,
|
||||||
Privileged: opts.privileged,
|
Privileged: opts.privileged,
|
||||||
PullPolicy: opts.pull,
|
PullPolicy: opts.pull,
|
||||||
Resources: api.ContainerResources{
|
Resources: api.ContainerResources{
|
||||||
CPU: opts.cpu.Value(),
|
CPU: opts.cpu.Value(),
|
||||||
Memory: opts.memory.Value(),
|
Memory: opts.memory.Value(),
|
||||||
|
MemoryReservation: opts.memoryReservation.Value(),
|
||||||
SharedMemory: opts.shmSize.Value(),
|
SharedMemory: opts.shmSize.Value(),
|
||||||
Ulimits: ulimits,
|
Ulimits: ulimits,
|
||||||
},
|
},
|
||||||
User: opts.user,
|
User: opts.user,
|
||||||
VolumeMounts: mounts,
|
VolumeMounts: mounts,
|
||||||
},
|
},
|
||||||
|
Labels: labels,
|
||||||
Mode: opts.mode,
|
Mode: opts.mode,
|
||||||
Name: opts.name,
|
Name: opts.name,
|
||||||
Placement: placement,
|
Placement: placement,
|
||||||
@@ -300,6 +329,24 @@ func parseEnv(env []string) (api.EnvVars, error) {
|
|||||||
return envVars, nil
|
return envVars, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseLabels parses labels from the command line arguments in the format "key=value" or just "key" for an empty value.
|
||||||
|
func parseLabels(labels []string) (map[string]string, error) {
|
||||||
|
if len(labels) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]string, len(labels))
|
||||||
|
for _, label := range labels {
|
||||||
|
key, value, _ := strings.Cut(label, "=")
|
||||||
|
if strings.TrimSpace(key) == "" {
|
||||||
|
return nil, fmt.Errorf("invalid label '%s': key must not be empty", label)
|
||||||
|
}
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// parseVolumeFlags parses volume flag values in Docker CLI format and returns VolumeSpecs and VolumeMounts.
|
// parseVolumeFlags parses volume flag values in Docker CLI format and returns VolumeSpecs and VolumeMounts.
|
||||||
// It handles both named volumes (volume_name:/container/path[:ro|volume-nocopy])
|
// It handles both named volumes (volume_name:/container/path[:ro|volume-nocopy])
|
||||||
// and bind mounts (/host/path:/container/path[:ro]).
|
// and bind mounts (/host/path:/container/path[:ro]).
|
||||||
|
|||||||
@@ -0,0 +1,423 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// validRunOptions returns the minimal options that produce a valid service spec.
|
||||||
|
func validRunOptions() runOptions {
|
||||||
|
return runOptions{
|
||||||
|
image: "nginx:1.27",
|
||||||
|
name: "web",
|
||||||
|
mode: api.ServiceModeReplicated,
|
||||||
|
pull: api.PullPolicyMissing,
|
||||||
|
replicas: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseLabels(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
labels, err := parseLabels(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, labels)
|
||||||
|
|
||||||
|
labels, err = parseLabels([]string{"tier=frontend", "empty", "list=a,b", "expr=a=b", "tier=backend"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, map[string]string{
|
||||||
|
"tier": "backend",
|
||||||
|
"empty": "",
|
||||||
|
"list": "a,b",
|
||||||
|
"expr": "a=b",
|
||||||
|
}, labels)
|
||||||
|
|
||||||
|
_, err = parseLabels([]string{"=value"})
|
||||||
|
require.ErrorContains(t, err, "key must not be empty")
|
||||||
|
|
||||||
|
_, err = parseLabels([]string{" \t =value"})
|
||||||
|
require.ErrorContains(t, err, "key must not be empty")
|
||||||
|
|
||||||
|
labels, err = parseLabels([]string{"key=value with spaces"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, map[string]string{"key": "value with spaces"}, labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareServiceSpec(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("minimal options", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(validRunOptions())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "web", spec.Name)
|
||||||
|
assert.Equal(t, api.ServiceModeReplicated, spec.Mode)
|
||||||
|
assert.EqualValues(t, 1, spec.Replicas)
|
||||||
|
assert.Equal(t, "nginx:1.27", spec.Container.Image)
|
||||||
|
assert.Equal(t, api.PullPolicyMissing, spec.Container.PullPolicy)
|
||||||
|
assert.Nil(t, spec.Caddy)
|
||||||
|
assert.Nil(t, spec.Labels)
|
||||||
|
assert.Nil(t, spec.Container.Labels)
|
||||||
|
assert.Nil(t, spec.Container.Entrypoint)
|
||||||
|
assert.Nil(t, spec.Container.Resources.Ulimits)
|
||||||
|
assert.Nil(t, spec.Placement.Machines)
|
||||||
|
assert.Empty(t, spec.Container.Env)
|
||||||
|
assert.Empty(t, spec.Ports)
|
||||||
|
assert.Empty(t, spec.Volumes)
|
||||||
|
assert.Empty(t, spec.Container.VolumeMounts)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("all options", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.command = []string{"nginx", "-g", "daemon off;"}
|
||||||
|
opts.env = []string{"FOO=bar", "EMPTY=", "EXPR=a=b"}
|
||||||
|
opts.labels = []string{"tier=frontend"}
|
||||||
|
opts.containerLabels = []string{"logging=enabled"}
|
||||||
|
opts.machines = []string{"m1,m2", "m3"}
|
||||||
|
opts.privileged = true
|
||||||
|
opts.pull = api.PullPolicyAlways
|
||||||
|
opts.publish = []string{"app.example.com:8080/https", "53:5353/udp@host"}
|
||||||
|
opts.replicas = 3
|
||||||
|
opts.ulimits = []string{"nofile=1024:2048", "nproc=65535"}
|
||||||
|
opts.user = "1000:1000"
|
||||||
|
opts.volumes = []string{"data:/var/lib/data:ro"}
|
||||||
|
require.NoError(t, opts.cpu.Set("0.5"))
|
||||||
|
require.NoError(t, opts.memory.Set("128m"))
|
||||||
|
require.NoError(t, opts.memoryReservation.Set("32m"))
|
||||||
|
require.NoError(t, opts.shmSize.Set("64m"))
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, api.ServiceSpec{
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Command: []string{"nginx", "-g", "daemon off;"},
|
||||||
|
Env: api.EnvVars{"FOO": "bar", "EMPTY": "", "EXPR": "a=b"},
|
||||||
|
Image: "nginx:1.27",
|
||||||
|
Labels: map[string]string{"logging": "enabled"},
|
||||||
|
Privileged: true,
|
||||||
|
PullPolicy: api.PullPolicyAlways,
|
||||||
|
Resources: api.ContainerResources{
|
||||||
|
CPU: 500 * api.MilliCore,
|
||||||
|
Memory: 128 * units.MiB,
|
||||||
|
MemoryReservation: 32 * units.MiB,
|
||||||
|
SharedMemory: 64 * units.MiB,
|
||||||
|
Ulimits: map[string]api.Ulimit{
|
||||||
|
"nofile": {Soft: 1024, Hard: 2048},
|
||||||
|
"nproc": {Soft: 65535, Hard: 65535},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
User: "1000:1000",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{VolumeName: "data", ContainerPath: "/var/lib/data", ReadOnly: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Labels: map[string]string{"tier": "frontend"},
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
Name: "web",
|
||||||
|
Placement: api.Placement{Machines: []string{"m1", "m2", "m3"}},
|
||||||
|
Ports: []api.PortSpec{
|
||||||
|
{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: api.ProtocolHTTPS,
|
||||||
|
Mode: api.PortModeIngress,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
PublishedPort: 53,
|
||||||
|
ContainerPort: 5353,
|
||||||
|
Protocol: api.ProtocolUDP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Replicas: 3,
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "data",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{Name: "data"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, spec)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("generates a name when not specified", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.name = ""
|
||||||
|
opts.image = "bitnami/nginx:latest"
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Regexp(t, `^nginx-[a-z0-9]{4}$`, spec.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("keeps the specified name", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.name = "custom"
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "custom", spec.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("global mode", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.mode = api.ServiceModeGlobal
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, api.ServiceModeGlobal, spec.Mode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("entrypoint", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.entrypoint = "/bin/sh"
|
||||||
|
opts.entrypointChanged = true
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []string{"/bin/sh"}, spec.Container.Entrypoint)
|
||||||
|
|
||||||
|
// An explicitly empty entrypoint resets the one from the image.
|
||||||
|
opts = validRunOptions()
|
||||||
|
opts.entrypointChanged = true
|
||||||
|
spec, err = prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []string{""}, spec.Container.Entrypoint)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("env without a value is omitted when not set locally", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.env = []string{"UNCLOUD_TEST_ENV_THAT_IS_NOT_SET"}
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotContains(t, spec.Container.Env, "UNCLOUD_TEST_ENV_THAT_IS_NOT_SET")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bind mount", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.volumes = []string{"/data/uploads:/app/uploads"}
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, spec.Volumes, 1)
|
||||||
|
assert.Regexp(t, `^bind-[a-z0-9]{4}$`, spec.Volumes[0].Name)
|
||||||
|
assert.Equal(t, api.VolumeTypeBind, spec.Volumes[0].Type)
|
||||||
|
require.NotNil(t, spec.Volumes[0].BindOptions)
|
||||||
|
assert.Equal(t, "/data/uploads", spec.Volumes[0].BindOptions.HostPath)
|
||||||
|
assert.True(t, spec.Volumes[0].BindOptions.CreateHostPath)
|
||||||
|
|
||||||
|
require.Len(t, spec.Container.VolumeMounts, 1)
|
||||||
|
assert.Equal(t, spec.Volumes[0].Name, spec.Container.VolumeMounts[0].VolumeName)
|
||||||
|
assert.Equal(t, "/app/uploads", spec.Container.VolumeMounts[0].ContainerPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("named volume mounted twice creates one volume", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.volumes = []string{"data:/a", "data:/b"}
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, spec.Volumes, 1)
|
||||||
|
assert.Len(t, spec.Container.VolumeMounts, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("caddyfile content is trimmed", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "Caddyfile")
|
||||||
|
require.NoError(t, os.WriteFile(path, []byte("\nexample.com {\n}\n\n"), 0o600))
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.caddyfile = path
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, spec.Caddy)
|
||||||
|
assert.Equal(t, "example.com {\n}", spec.Caddy.Config)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty caddyfile is ignored", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "Caddyfile")
|
||||||
|
require.NoError(t, os.WriteFile(path, []byte(" \n"), 0o600))
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.caddyfile = path
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, spec.Caddy)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("caddyfile can be used with host ports", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "Caddyfile")
|
||||||
|
require.NoError(t, os.WriteFile(path, []byte("example.com {\n}"), 0o600))
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.caddyfile = path
|
||||||
|
opts.publish = []string{"53:5353/udp@host"}
|
||||||
|
|
||||||
|
_, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareServiceSpec_LocalEnv(t *testing.T) {
|
||||||
|
// Not parallel because t.Setenv changes the environment of the whole process.
|
||||||
|
t.Setenv("UNCLOUD_TEST_LOCAL_ENV", "from-local")
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.env = []string{"UNCLOUD_TEST_LOCAL_ENV"}
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "from-local", spec.Container.Env["UNCLOUD_TEST_LOCAL_ENV"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareServiceSpec_Invalid(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
caddyfile := filepath.Join(t.TempDir(), "Caddyfile")
|
||||||
|
require.NoError(t, os.WriteFile(caddyfile, []byte("example.com {\n}"), 0o600))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
modify func(opts *runOptions)
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid image",
|
||||||
|
modify: func(opts *runOptions) { opts.image = "INVALID IMAGE" },
|
||||||
|
wantErr: "invalid image",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid name",
|
||||||
|
modify: func(opts *runOptions) { opts.name = "Not_Valid" },
|
||||||
|
wantErr: "invalid service name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid mode",
|
||||||
|
modify: func(opts *runOptions) { opts.mode = "unknown" },
|
||||||
|
wantErr: "invalid replication mode: 'unknown'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid pull policy",
|
||||||
|
modify: func(opts *runOptions) { opts.pull = "sometimes" },
|
||||||
|
wantErr: "invalid pull policy: 'sometimes'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid env",
|
||||||
|
modify: func(opts *runOptions) { opts.env = []string{"=value"} },
|
||||||
|
wantErr: "invalid environment variable: '=value'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid port",
|
||||||
|
modify: func(opts *runOptions) { opts.publish = []string{"not-a-port"} },
|
||||||
|
wantErr: "invalid service port 'not-a-port'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid ulimit",
|
||||||
|
modify: func(opts *runOptions) { opts.ulimits = []string{"nofile"} },
|
||||||
|
wantErr: "invalid ulimit 'nofile'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "volume without a container path",
|
||||||
|
modify: func(opts *runOptions) { opts.volumes = []string{"data"} },
|
||||||
|
wantErr: "invalid volume mount 'data'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "volume with a relative container path",
|
||||||
|
modify: func(opts *runOptions) { opts.volumes = []string{"data:relative"} },
|
||||||
|
wantErr: "must be absolute path",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "volume with an unknown option",
|
||||||
|
modify: func(opts *runOptions) { opts.volumes = []string{"data:/data:unknown"} },
|
||||||
|
wantErr: "invalid option: 'unknown'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing caddyfile",
|
||||||
|
modify: func(opts *runOptions) { opts.caddyfile = filepath.Join(os.TempDir(), "uncloud-test-no-such-file") },
|
||||||
|
wantErr: "read Caddyfile",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "caddyfile with an ingress port",
|
||||||
|
modify: func(opts *runOptions) {
|
||||||
|
opts.caddyfile = caddyfile
|
||||||
|
opts.publish = []string{"app.example.com:8080/https"}
|
||||||
|
},
|
||||||
|
wantErr: "cannot be specified simultaneously",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service label without a key",
|
||||||
|
modify: func(opts *runOptions) { opts.labels = []string{"=value"} },
|
||||||
|
wantErr: "key must not be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "container label without a key",
|
||||||
|
modify: func(opts *runOptions) { opts.containerLabels = []string{"=value"} },
|
||||||
|
wantErr: "key must not be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service label with whitespace",
|
||||||
|
modify: func(opts *runOptions) { opts.labels = []string{"my label=value"} },
|
||||||
|
wantErr: "invalid service labels: label 'my label' must not contain whitespace",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "container label with whitespace",
|
||||||
|
modify: func(opts *runOptions) { opts.containerLabels = []string{"my label=value"} },
|
||||||
|
wantErr: "invalid container labels: label 'my label' must not contain whitespace",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reserved service label",
|
||||||
|
modify: func(opts *runOptions) { opts.labels = []string{api.LabelManaged + "=true"} },
|
||||||
|
wantErr: "invalid service labels",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reserved container label",
|
||||||
|
modify: func(opts *runOptions) { opts.containerLabels = []string{api.LabelManaged + "=true"} },
|
||||||
|
wantErr: "invalid container labels",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
tt.modify(&opts)
|
||||||
|
|
||||||
|
_, err := prepareServiceSpec(opts)
|
||||||
|
require.ErrorContains(t, err, tt.wantErr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
dockeropts "github.com/docker/cli/opts"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/completion"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type updateOptions struct {
|
||||||
|
service string
|
||||||
|
|
||||||
|
cpu dockeropts.NanoCPUs
|
||||||
|
cpuChanged bool
|
||||||
|
memory dockeropts.MemBytes
|
||||||
|
memoryChanged bool
|
||||||
|
memoryReservation dockeropts.MemBytes
|
||||||
|
reservationChanged bool
|
||||||
|
labelAdd []string
|
||||||
|
labelRm []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdateCommand() *cobra.Command {
|
||||||
|
opts := updateOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "update SERVICE",
|
||||||
|
Short: "Update a service in place without recreating its containers.",
|
||||||
|
Long: `Update service configuration in place without recreating its containers.
|
||||||
|
|
||||||
|
A CPU or memory limit can be changed but not removed. To remove a limit or change anything else,
|
||||||
|
redeploy the service with 'uc deploy'`,
|
||||||
|
Example: ` # Change the CPU and memory limits.
|
||||||
|
uc service update web --cpu 0.5 --memory 512m
|
||||||
|
|
||||||
|
# Add or change a service label and remove another one.
|
||||||
|
uc service update web --label-add tier=frontend --label-rm legacy`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
|
opts.service = args[0]
|
||||||
|
opts.cpuChanged = cmd.Flag("cpu").Changed
|
||||||
|
opts.memoryChanged = cmd.Flag("memory").Changed
|
||||||
|
opts.reservationChanged = cmd.Flag("memory-reservation").Changed
|
||||||
|
|
||||||
|
return update(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command, args []string, toComplete string,
|
||||||
|
) ([]cobra.Completion, cobra.ShellCompDirective) {
|
||||||
|
if len(args) > 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return completion.Services(cmd.Context(), uncli, args, toComplete)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().Var(&opts.cpu, "cpu",
|
||||||
|
"Maximum number of CPU cores a service container can use.\n"+
|
||||||
|
"Fractional values are allowed: 0.5 for half a core or 2.25 for two and a quarter cores.")
|
||||||
|
cmd.Flags().Var(&opts.memory, "memory",
|
||||||
|
"Maximum amount of memory a service container can use. Value is a positive integer with\n"+
|
||||||
|
"optional unit suffix (b, k, m, g). Default unit is bytes if no suffix specified.\n"+
|
||||||
|
"Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)")
|
||||||
|
cmd.Flags().Var(&opts.memoryReservation, "memory-reservation",
|
||||||
|
"Minimum amount of memory reserved for a service container. Value is a positive integer with\n"+
|
||||||
|
"optional unit suffix (b, k, m, g). Default unit is bytes if no suffix specified.")
|
||||||
|
cmd.Flags().StringArrayVar(&opts.labelAdd, "label-add", nil,
|
||||||
|
"Add or change a service label. Can be specified multiple times.\n"+
|
||||||
|
"Format: key=value or just key for an empty value.")
|
||||||
|
cmd.Flags().StringArrayVar(&opts.labelRm, "label-rm", nil,
|
||||||
|
"Remove a service label by its key. Can be specified multiple times.")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(ctx context.Context, uncli *cli.CLI, opts updateOptions) error {
|
||||||
|
if !opts.cpuChanged && !opts.memoryChanged && !opts.reservationChanged &&
|
||||||
|
len(opts.labelAdd) == 0 && len(opts.labelRm) == 0 {
|
||||||
|
return errors.New("no changes specified, set at least one of the flags: " +
|
||||||
|
"--cpu, --memory, --memory-reservation, --label-add, --label-rm")
|
||||||
|
}
|
||||||
|
|
||||||
|
labelAdd, err := parseLabels(opts.labelAdd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = api.ValidateLabels(labelAdd); err != nil {
|
||||||
|
return fmt.Errorf("invalid service labels: %w", err)
|
||||||
|
}
|
||||||
|
for _, key := range opts.labelRm {
|
||||||
|
if _, ok := labelAdd[key]; ok {
|
||||||
|
return fmt.Errorf("label '%s' cannot be both added and removed", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
svc, err := clusterClient.InspectService(ctx, opts.service)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect service '%s': %w", opts.service, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all containers before changing any of them to avoid a partially updated service when a limit can't be
|
||||||
|
// changed in place. Containers may have different specs after a failed deployment so each one is updated from
|
||||||
|
// its own spec.
|
||||||
|
updates := make(map[string]api.UpdateContainerOptions, len(svc.Containers))
|
||||||
|
for _, mc := range svc.Containers {
|
||||||
|
spec := applyServiceUpdate(mc.Container.ServiceSpec, opts, labelAdd)
|
||||||
|
|
||||||
|
resources := mc.Container.AppliedResources()
|
||||||
|
if err = resources.ValidateUpdateInPlace(spec.Container.Resources); err != nil {
|
||||||
|
return fmt.Errorf("container '%s' on machine '%s' can't be updated without recreating it: %w",
|
||||||
|
stringid.TruncateID(mc.Container.ID), mc.MachineName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
containerOpts := deploy.ContainerUpdateOptions(mc.Container, spec)
|
||||||
|
if containerOpts != (api.UpdateContainerOptions{}) {
|
||||||
|
updates[mc.Container.ID] = containerOpts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updates) == 0 {
|
||||||
|
fmt.Printf("Service %s is already up to date. No changes required.\n", tui.NameStyle.Render(svc.Name))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
// Keep updating the remaining containers if one fails, for example, because its machine is down.
|
||||||
|
// Running the command again only updates the containers that still differ.
|
||||||
|
var errs []error
|
||||||
|
for id, containerOpts := range updates {
|
||||||
|
if err := clusterClient.UpdateContainer(ctx, svc.ID, id, containerOpts); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("update container '%s': %w", stringid.TruncateID(id), err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}, uncli.ProgressOut(), "Updating service "+tui.NameStyle.Render(svc.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyServiceUpdate returns a copy of the spec with the requested changes applied.
|
||||||
|
func applyServiceUpdate(spec api.ServiceSpec, opts updateOptions, labelAdd map[string]string) api.ServiceSpec {
|
||||||
|
updated := spec.Clone()
|
||||||
|
|
||||||
|
if opts.cpuChanged {
|
||||||
|
updated.Container.Resources.CPU = opts.cpu.Value()
|
||||||
|
}
|
||||||
|
if opts.memoryChanged {
|
||||||
|
updated.Container.Resources.Memory = opts.memory.Value()
|
||||||
|
}
|
||||||
|
if opts.reservationChanged {
|
||||||
|
updated.Container.Resources.MemoryReservation = opts.memoryReservation.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(labelAdd) > 0 && updated.Labels == nil {
|
||||||
|
updated.Labels = make(map[string]string, len(labelAdd))
|
||||||
|
}
|
||||||
|
maps.Copy(updated.Labels, labelAdd)
|
||||||
|
|
||||||
|
for key := range updated.Labels {
|
||||||
|
if slices.Contains(opts.labelRm, key) {
|
||||||
|
delete(updated.Labels, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestApplyServiceUpdate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
newSpec := func() api.ServiceSpec {
|
||||||
|
return api.ServiceSpec{
|
||||||
|
Name: "web",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "nginx",
|
||||||
|
Labels: map[string]string{"container": "keep"},
|
||||||
|
Resources: api.ContainerResources{
|
||||||
|
CPU: 100 * api.MilliCore,
|
||||||
|
Memory: 1024,
|
||||||
|
MemoryReservation: 512,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Labels: map[string]string{"tier": "frontend", "legacy": "true"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("adds, changes and removes labels", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
spec := newSpec()
|
||||||
|
updated := applyServiceUpdate(spec, updateOptions{labelRm: []string{"legacy", "missing"}},
|
||||||
|
map[string]string{"tier": "backend", "team": "core"})
|
||||||
|
|
||||||
|
assert.Equal(t, map[string]string{"tier": "backend", "team": "core"}, updated.Labels)
|
||||||
|
assert.Equal(t, map[string]string{"container": "keep"}, updated.Container.Labels)
|
||||||
|
assert.Equal(t, spec.Container.Resources, updated.Container.Resources)
|
||||||
|
assert.Equal(t, map[string]string{"tier": "frontend", "legacy": "true"}, spec.Labels,
|
||||||
|
"Expected the original spec to stay unchanged")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adds labels to a spec without labels", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
spec := newSpec()
|
||||||
|
spec.Labels = nil
|
||||||
|
updated := applyServiceUpdate(spec, updateOptions{}, map[string]string{"tier": "backend"})
|
||||||
|
|
||||||
|
assert.Equal(t, map[string]string{"tier": "backend"}, updated.Labels)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("changes only requested limits", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := updateOptions{cpuChanged: true}
|
||||||
|
require.NoError(t, opts.cpu.Set("0.5"))
|
||||||
|
|
||||||
|
updated := applyServiceUpdate(newSpec(), opts, nil)
|
||||||
|
|
||||||
|
assert.EqualValues(t, 500*api.MilliCore, updated.Container.Resources.CPU)
|
||||||
|
assert.EqualValues(t, 1024, updated.Container.Resources.Memory)
|
||||||
|
assert.EqualValues(t, 512, updated.Container.Resources.MemoryReservation)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sends only changed fields to the container", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := updateOptions{memoryChanged: true}
|
||||||
|
require.NoError(t, opts.memory.Set("2048"))
|
||||||
|
|
||||||
|
ctr := api.ServiceContainer{ServiceSpec: newSpec()}
|
||||||
|
containerOpts := deploy.ContainerUpdateOptions(ctr, applyServiceUpdate(ctr.ServiceSpec, opts, nil))
|
||||||
|
|
||||||
|
require.NotNil(t, containerOpts.Memory)
|
||||||
|
assert.EqualValues(t, 2048, *containerOpts.Memory)
|
||||||
|
assert.Nil(t, containerOpts.CPU)
|
||||||
|
assert.Nil(t, containerOpts.MemoryReservation)
|
||||||
|
assert.Nil(t, containerOpts.ServiceLabels)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("removing the last label clears service labels", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctr := api.ServiceContainer{ServiceSpec: newSpec()}
|
||||||
|
containerOpts := deploy.ContainerUpdateOptions(ctr, applyServiceUpdate(
|
||||||
|
ctr.ServiceSpec, updateOptions{labelRm: []string{"tier", "legacy"}}, nil,
|
||||||
|
))
|
||||||
|
|
||||||
|
require.NotNil(t, containerOpts.ServiceLabels)
|
||||||
|
assert.Empty(t, *containerOpts.ServiceLabels)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no changes produce empty container options", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctr := api.ServiceContainer{ServiceSpec: newSpec()}
|
||||||
|
containerOpts := deploy.ContainerUpdateOptions(ctr, applyServiceUpdate(
|
||||||
|
ctr.ServiceSpec, updateOptions{}, map[string]string{"tier": "frontend"},
|
||||||
|
))
|
||||||
|
|
||||||
|
assert.Equal(t, api.UpdateContainerOptions{}, containerOpts)
|
||||||
|
})
|
||||||
|
}
|
||||||
+22
-22
@@ -1,6 +1,6 @@
|
|||||||
module github.com/psviderski/uncloud/experiment
|
module github.com/psviderski/uncloud/experiment
|
||||||
|
|
||||||
go 1.26
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dgraph-io/badger/v3 v3.2103.5
|
github.com/dgraph-io/badger/v3 v3.2103.5
|
||||||
@@ -18,10 +18,10 @@ require (
|
|||||||
github.com/psviderski/uncloud v0.0.0
|
github.com/psviderski/uncloud v0.0.0
|
||||||
github.com/siderolabs/discovery-api v0.1.4
|
github.com/siderolabs/discovery-api v0.1.4
|
||||||
github.com/siderolabs/discovery-client v0.1.9
|
github.com/siderolabs/discovery-client v0.1.9
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.28.0
|
||||||
golang.org/x/net v0.43.0
|
golang.org/x/net v0.59.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
google.golang.org/grpc v1.74.2
|
google.golang.org/grpc v1.81.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -79,18 +79,18 @@ require (
|
|||||||
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect
|
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect
|
||||||
github.com/jbenet/goprocess v0.1.4 // indirect
|
github.com/jbenet/goprocess v0.1.4 // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.19.1 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||||
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
||||||
github.com/libp2p/go-libp2p v0.35.4 // indirect
|
github.com/libp2p/go-libp2p v0.35.4 // indirect
|
||||||
github.com/libp2p/go-libp2p-pubsub v0.11.0 // indirect
|
github.com/libp2p/go-libp2p-pubsub v0.11.0 // indirect
|
||||||
github.com/libp2p/go-msgio v0.3.0 // indirect
|
github.com/libp2p/go-msgio v0.3.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
github.com/mdlayher/socket v0.5.1 // indirect
|
||||||
github.com/miekg/dns v1.1.65 // indirect
|
github.com/miekg/dns v1.1.73 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
github.com/mitchellh/cli v1.1.5 // indirect
|
github.com/mitchellh/cli v1.1.5 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
@@ -113,30 +113,30 @@ require (
|
|||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/siderolabs/gen v0.4.8 // indirect
|
github.com/siderolabs/gen v0.4.8 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/spf13/cast v1.7.0 // indirect
|
github.com/spf13/cast v1.7.0 // indirect
|
||||||
github.com/vishvananda/netlink v1.3.1 // indirect
|
github.com/vishvananda/netlink v1.3.1 // indirect
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||||
golang.org/x/crypto v0.41.0 // indirect
|
golang.org/x/crypto v0.57.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||||
golang.org/x/mod v0.27.0 // indirect
|
golang.org/x/mod v0.41.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.23.0 // indirect
|
||||||
golang.org/x/sys v0.45.0 // indirect
|
golang.org/x/sys v0.48.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.42.0 // indirect
|
||||||
golang.org/x/time v0.11.0 // indirect
|
golang.org/x/time v0.16.0 // indirect
|
||||||
golang.org/x/tools v0.36.0 // indirect
|
golang.org/x/tools v0.50.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 // indirect
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
google.golang.org/protobuf v1.36.12 // indirect
|
||||||
gotest.tools/v3 v3.5.2 // indirect
|
gotest.tools/v3 v3.5.2 // indirect
|
||||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
|
||||||
lukechampine.com/blake3 v1.3.0 // indirect
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
|
|||||||
@@ -302,8 +302,10 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
|||||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
|
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
|
||||||
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
|
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
|
||||||
@@ -349,6 +351,7 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
|
|||||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||||
@@ -368,6 +371,7 @@ github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKju
|
|||||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||||
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
|
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
|
||||||
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
|
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
|
||||||
|
github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
|
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
|
||||||
@@ -529,6 +533,7 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
|
|||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||||
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
||||||
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||||
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
|
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
|
||||||
@@ -581,16 +586,20 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
|||||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||||
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc=
|
go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc=
|
||||||
go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||||
@@ -607,6 +616,7 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
|
|||||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
@@ -622,6 +632,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
|
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||||
@@ -635,6 +646,7 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||||
|
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -654,6 +666,7 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
|
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -666,6 +679,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -700,6 +714,7 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/sys v0.48.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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
@@ -711,8 +726,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
|
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
@@ -727,6 +744,7 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
|
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -745,6 +763,7 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
|
|||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
@@ -753,6 +772,7 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
|
|||||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||||
|
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
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-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 v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
@@ -764,6 +784,7 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
|
|||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
|
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ go 1.26.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
charm.land/bubbles/v2 v2.0.0
|
charm.land/bubbles/v2 v2.0.0
|
||||||
charm.land/bubbletea/v2 v2.0.2
|
charm.land/bubbletea/v2 v2.0.10
|
||||||
charm.land/huh/v2 v2.0.1
|
charm.land/huh/v2 v2.0.1
|
||||||
charm.land/lipgloss/v2 v2.0.1
|
charm.land/lipgloss/v2 v2.0.1
|
||||||
github.com/BurntSushi/toml v1.6.0
|
github.com/BurntSushi/toml v1.6.0
|
||||||
@@ -34,6 +34,7 @@ require (
|
|||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/mattn/go-shellwords v1.0.12
|
github.com/mattn/go-shellwords v1.0.12
|
||||||
|
github.com/mholt/acmez/v3 v3.1.6
|
||||||
github.com/miekg/dns v1.1.73
|
github.com/miekg/dns v1.1.73
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/moby/term v0.5.2
|
github.com/moby/term v0.5.2
|
||||||
@@ -61,27 +62,14 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cel.dev/expr v0.25.1 // indirect
|
|
||||||
cloud.google.com/go/auth v0.20.0 // indirect
|
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
|
||||||
dario.cat/mergo v1.0.2 // indirect
|
|
||||||
filippo.io/bigmod v0.1.0 // indirect
|
|
||||||
filippo.io/edwards25519 v1.2.0 // indirect
|
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
||||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||||
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e // indirect
|
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e // indirect
|
||||||
github.com/KimMachineGun/automemlimit v0.7.5 // indirect
|
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/Microsoft/hcsshim v0.13.0 // indirect
|
github.com/Microsoft/hcsshim v0.13.0 // indirect
|
||||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
|
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
|
||||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
|
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.7 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.41.7 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.17 // indirect
|
github.com/aws/aws-sdk-go-v2/config v1.32.17 // indirect
|
||||||
@@ -101,20 +89,16 @@ require (
|
|||||||
github.com/buger/goterm v1.0.4 // indirect
|
github.com/buger/goterm v1.0.4 // indirect
|
||||||
github.com/caddyserver/zerossl v0.1.5 // indirect
|
github.com/caddyserver/zerossl v0.1.5 // indirect
|
||||||
github.com/catppuccin/go v0.3.0 // indirect
|
github.com/catppuccin/go v0.3.0 // indirect
|
||||||
github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect
|
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cespare/xxhash v1.1.0 // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 // indirect
|
github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 // indirect
|
||||||
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
|
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||||
github.com/chzyer/readline v1.5.1 // indirect
|
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.3 // indirect
|
|
||||||
github.com/containerd/cgroups/v3 v3.0.5 // indirect
|
github.com/containerd/cgroups/v3 v3.0.5 // indirect
|
||||||
github.com/containerd/console v1.0.5 // indirect
|
github.com/containerd/console v1.0.5 // indirect
|
||||||
github.com/containerd/containerd/api v1.9.0 // indirect
|
github.com/containerd/containerd/api v1.9.0 // indirect
|
||||||
@@ -127,13 +111,8 @@ require (
|
|||||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||||
github.com/containerd/ttrpc v1.2.7 // indirect
|
github.com/containerd/ttrpc v1.2.7 // indirect
|
||||||
github.com/containerd/typeurl/v2 v2.2.3 // indirect
|
github.com/containerd/typeurl/v2 v2.2.3 // indirect
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/dgraph-io/badger v1.6.2 // indirect
|
|
||||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
|
||||||
github.com/dgraph-io/ristretto v0.2.0 // indirect
|
|
||||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/distribution/distribution/v3 v3.0.0 // indirect
|
github.com/distribution/distribution/v3 v3.0.0 // indirect
|
||||||
github.com/dlclark/regexp2 v1.12.0 // indirect
|
github.com/dlclark/regexp2 v1.12.0 // indirect
|
||||||
@@ -151,14 +130,11 @@ require (
|
|||||||
github.com/fsnotify/fsevents v0.2.0 // indirect
|
github.com/fsnotify/fsevents v0.2.0 // indirect
|
||||||
github.com/fvbommel/sortorder v1.1.0 // indirect
|
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
|
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||||
github.com/go-openapi/swag v0.23.0 // indirect
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
github.com/gofrs/flock v0.12.1 // indirect
|
github.com/gofrs/flock v0.12.1 // indirect
|
||||||
@@ -166,14 +142,9 @@ require (
|
|||||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
|
||||||
github.com/google/cel-go v0.28.1 // indirect
|
|
||||||
github.com/google/gnostic-models v0.6.8 // indirect
|
github.com/google/gnostic-models v0.6.8 // indirect
|
||||||
github.com/google/gofuzz v1.2.0 // indirect
|
github.com/google/gofuzz v1.2.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
|
|
||||||
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
|
|
||||||
github.com/gorilla/handlers v1.5.2 // indirect
|
github.com/gorilla/handlers v1.5.2 // indirect
|
||||||
github.com/gorilla/mux v1.8.1 // indirect
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
@@ -184,15 +155,11 @@ require (
|
|||||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||||
github.com/hashicorp/golang-lru/arc/v2 v2.0.7 // indirect
|
github.com/hashicorp/golang-lru/arc/v2 v2.0.7 // indirect
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
||||||
github.com/in-toto/in-toto-golang v0.9.0 // indirect
|
github.com/in-toto/in-toto-golang v0.9.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect
|
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect
|
||||||
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect
|
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
|
||||||
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
|
||||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
@@ -205,7 +172,6 @@ require (
|
|||||||
github.com/libdns/libdns v1.1.1 // indirect
|
github.com/libdns/libdns v1.1.1 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||||
@@ -213,13 +179,9 @@ require (
|
|||||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
github.com/mdlayher/socket v0.5.1 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/mholt/acmez/v3 v3.1.6 // indirect
|
|
||||||
github.com/miekg/pkcs11 v1.1.2 // indirect
|
github.com/miekg/pkcs11 v1.1.2 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
|
||||||
github.com/moby/buildkit v0.25.0 // indirect
|
github.com/moby/buildkit v0.25.0 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/go-archive v0.1.0 // indirect
|
github.com/moby/go-archive v0.1.0 // indirect
|
||||||
@@ -245,9 +207,7 @@ require (
|
|||||||
github.com/opencontainers/runtime-spec v1.2.1 // indirect
|
github.com/opencontainers/runtime-spec v1.2.1 // indirect
|
||||||
github.com/opencontainers/selinux v1.12.0 // indirect
|
github.com/opencontainers/selinux v1.12.0 // indirect
|
||||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
|
||||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
github.com/pires/go-proxyproto v0.12.0 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||||
github.com/prometheus/client_model v0.6.3 // indirect
|
github.com/prometheus/client_model v0.6.3 // indirect
|
||||||
@@ -261,28 +221,17 @@ require (
|
|||||||
github.com/redis/go-redis/v9 v9.7.3 // indirect
|
github.com/redis/go-redis/v9 v9.7.3 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/rs/xid v1.6.0 // indirect
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||||
github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect
|
github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect
|
||||||
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect
|
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect
|
||||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
|
||||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
|
||||||
github.com/slackhq/nebula v1.10.3 // indirect
|
github.com/spf13/afero v1.14.0 // indirect
|
||||||
github.com/smallstep/certificates v0.30.2 // indirect
|
github.com/spf13/viper v1.12.0 // indirect
|
||||||
github.com/smallstep/cli-utils v0.12.2 // indirect
|
|
||||||
github.com/smallstep/linkedca v0.25.0 // indirect
|
|
||||||
github.com/smallstep/nosql v0.8.0 // indirect
|
|
||||||
github.com/smallstep/pkcs7 v0.2.1 // indirect
|
|
||||||
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 // indirect
|
|
||||||
github.com/smallstep/truststore v0.13.0 // indirect
|
|
||||||
github.com/spf13/cast v1.7.0 // indirect
|
|
||||||
github.com/stretchr/objx v0.5.3 // indirect
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
github.com/subosito/gotenv v1.4.1 // indirect
|
||||||
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 // indirect
|
|
||||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||||
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect
|
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect
|
||||||
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect
|
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect
|
||||||
@@ -290,7 +239,6 @@ require (
|
|||||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect
|
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect
|
||||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
|
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
|
||||||
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
|
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
|
||||||
github.com/urfave/cli v1.22.17 // indirect
|
|
||||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
@@ -298,7 +246,6 @@ require (
|
|||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
github.com/zclconf/go-cty v1.17.0 // indirect
|
github.com/zclconf/go-cty v1.17.0 // indirect
|
||||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||||
go.etcd.io/bbolt v1.4.3 // indirect
|
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect
|
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect
|
||||||
@@ -325,29 +272,22 @@ require (
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||||
go.step.sm/crypto v0.81.0 // indirect
|
|
||||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
|
||||||
go.uber.org/mock v0.6.0 // indirect
|
go.uber.org/mock v0.6.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.28.0 // indirect
|
go.uber.org/zap v1.28.0 // indirect
|
||||||
go.uber.org/zap/exp v0.3.0 // indirect
|
go.uber.org/zap/exp v0.3.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
|
||||||
golang.org/x/oauth2 v0.36.0 // indirect
|
golang.org/x/oauth2 v0.36.0 // indirect
|
||||||
golang.org/x/text v0.42.0 // indirect
|
golang.org/x/text v0.42.0 // indirect
|
||||||
golang.org/x/time v0.16.0 // indirect
|
golang.org/x/time v0.16.0 // indirect
|
||||||
golang.org/x/tools v0.50.0 // indirect
|
golang.org/x/tools v0.50.0 // indirect
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||||
google.golang.org/api v0.277.0 // indirect
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
|
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
howett.net/plist v1.0.0 // indirect
|
|
||||||
k8s.io/api v0.32.3 // indirect
|
k8s.io/api v0.32.3 // indirect
|
||||||
k8s.io/apimachinery v0.32.3 // indirect
|
k8s.io/apimachinery v0.32.3 // indirect
|
||||||
k8s.io/client-go v0.32.3 // indirect
|
k8s.io/client-go v0.32.3 // indirect
|
||||||
@@ -361,5 +301,3 @@ require (
|
|||||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
|
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
|
||||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace charm.land/bubbletea/v2 => github.com/unlabs-dev/bubbletea/v2 v2.0.8-uncloud.1
|
|
||||||
|
|||||||
@@ -1,32 +1,14 @@
|
|||||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
|
||||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
|
||||||
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
|
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
|
||||||
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
|
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
|
||||||
|
charm.land/bubbletea/v2 v2.0.10 h1:oolvo20VBpI0PfqE7iFjkZ1bx0WpmXGfnKz5Yldjq5o=
|
||||||
|
charm.land/bubbletea/v2 v2.0.10/go.mod h1:QOatcnhOjYIfxzUSTz6raF7Ex4R/rIuHa3SnBdCCpMc=
|
||||||
charm.land/huh/v2 v2.0.1 h1:9vhBjlIDuikdPKH+qnoG++GERVxqY0Lkv14xW57lj98=
|
charm.land/huh/v2 v2.0.1 h1:9vhBjlIDuikdPKH+qnoG++GERVxqY0Lkv14xW57lj98=
|
||||||
charm.land/huh/v2 v2.0.1/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
|
charm.land/huh/v2 v2.0.1/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
|
||||||
charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=
|
charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=
|
||||||
charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
|
charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
|
||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
|
||||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
|
||||||
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
|
|
||||||
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
|
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
|
||||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
|
||||||
cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U=
|
|
||||||
cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY=
|
|
||||||
cloud.google.com/go/kms v1.31.0 h1:LS8N92OxFDgOLg5NCo3OmbvjtQAIVT5gUHVLKIDHaFE=
|
|
||||||
cloud.google.com/go/kms v1.31.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U=
|
|
||||||
cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY=
|
|
||||||
cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E=
|
|
||||||
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
||||||
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
||||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
|
||||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
|
||||||
filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8=
|
|
||||||
filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI=
|
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
@@ -34,30 +16,19 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af
|
|||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
|
|
||||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
|
||||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/DeRuina/timberjack v1.4.2 h1:4bKlzhKdsR+2oNkgef9mqb4n11ICow8VK88RfzJPzN8=
|
|
||||||
github.com/DeRuina/timberjack v1.4.2/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
|
|
||||||
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0=
|
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0=
|
||||||
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw=
|
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw=
|
||||||
github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
|
|
||||||
github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
|
|
||||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
|
||||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
|
||||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
|
||||||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
@@ -66,9 +37,6 @@ github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWS
|
|||||||
github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok=
|
github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok=
|
||||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
||||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
|
||||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
|
||||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
|
||||||
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d h1:hi6J4K6DKrR4/ljxn6SF6nURyu785wKMuQcjt7H3VCQ=
|
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d h1:hi6J4K6DKrR4/ljxn6SF6nURyu785wKMuQcjt7H3VCQ=
|
||||||
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
||||||
@@ -83,15 +51,10 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
|||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
|
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
|
||||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=
|
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
|
||||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
|
|
||||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
|
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
|
||||||
@@ -112,8 +75,6 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZL
|
|||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
|
||||||
github.com/aws/aws-sdk-go-v2/service/kms v1.51.1 h1:zuSf4olLKZW8cF/W9Y5wvGT+/0raY/3kVp49KsGs0QY=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/kms v1.51.1/go.mod h1:Y0+uxvxz6ib4KktRdK0V4X45Vcs/JyYoz8H71pO8xeI=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc=
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
|
||||||
@@ -158,22 +119,18 @@ github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7l
|
|||||||
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||||
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||||
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||||
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
|
|
||||||
github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=
|
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
|
||||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||||
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 h1:FpSYhY28ucg9ZRr+2wj67FAQ0Ey5yiK0072PmRDJNek=
|
github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 h1:3FmWoGNWK4STvqg0O0Aeav2T7rodWJAPeF0QpH+8gFw=
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654/go.mod h1:hFpumms29Smx3LStRfku8vcCTBe1Kq8aCXtHUJa3mjY=
|
github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7/go.mod h1:f/jRa757WUmaOZrbPspXymbg/GnbF+rwe4OLsG7aXYo=
|
||||||
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
||||||
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
||||||
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
|
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
|
||||||
@@ -194,15 +151,6 @@ github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2
|
|||||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||||
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
|
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
|
||||||
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
|
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
|
||||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
|
||||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
|
||||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
|
||||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
|
||||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
|
||||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||||
@@ -210,8 +158,6 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ
|
|||||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
|
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
|
||||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
|
||||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
|
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
|
||||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
|
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
|
||||||
@@ -247,14 +193,8 @@ github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRq
|
|||||||
github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
|
github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
|
||||||
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
|
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
|
||||||
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
|
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
|
||||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
|
||||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
|
||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
|
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
|
||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
@@ -269,17 +209,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc
|
|||||||
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
|
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
|
||||||
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||||
github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
|
|
||||||
github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
|
|
||||||
github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o=
|
|
||||||
github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk=
|
|
||||||
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
|
||||||
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
|
||||||
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
|
|
||||||
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
|
|
||||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
|
||||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
|
|
||||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM=
|
github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM=
|
||||||
@@ -317,7 +246,6 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
|
|||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
|
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
|
||||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
|
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
|
||||||
@@ -332,8 +260,6 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
|
|||||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
|
||||||
github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c=
|
github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c=
|
||||||
github.com/fsnotify/fsevents v0.2.0/go.mod h1:B3eEk39i4hz8y1zaWS/wPrAP4O6wkIl7HQwKBr1qH/w=
|
github.com/fsnotify/fsevents v0.2.0/go.mod h1:B3eEk39i4hz8y1zaWS/wPrAP4O6wkIl7HQwKBr1qH/w=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
@@ -343,8 +269,6 @@ github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQ
|
|||||||
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
|
|
||||||
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
@@ -403,13 +327,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
|
|||||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
||||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
|
||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
||||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
|
||||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
|
||||||
github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM=
|
|
||||||
github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
|
|
||||||
github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
||||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
|
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
|
||||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
|
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
|
||||||
@@ -422,33 +339,20 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
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/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
|
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
|
||||||
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
|
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
|
||||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
|
||||||
github.com/google/go-tpm-tools v0.4.8 h1:V4oIYyAD3BykOycwYQzO29WefDouQMTsYZqmG3HxOfM=
|
|
||||||
github.com/google/go-tpm-tools v0.4.8/go.mod h1:4DfiOtiS1KppJjwf1+tqtW4K3PrCJjAAqFKj/TYTJKg=
|
|
||||||
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
|
|
||||||
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
|
||||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas=
|
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
|
||||||
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
|
|
||||||
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
|
|
||||||
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||||
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||||
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
@@ -473,7 +377,6 @@ github.com/hashicorp/golang-lru/arc/v2 v2.0.7 h1:QxkVTxwColcduO+LP7eJO56r2hFiG8z
|
|||||||
github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc=
|
github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
|
||||||
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
|
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
|
||||||
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
|
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
@@ -481,8 +384,6 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo
|
|||||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
|
||||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
|
||||||
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
|
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
|
||||||
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
|
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
@@ -492,15 +393,6 @@ github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaB
|
|||||||
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4=
|
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4=
|
||||||
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 h1:i2fYnDurfLlJH8AyyMOnkLHnHeP8Ff/DDpuZA/D3bPo=
|
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 h1:i2fYnDurfLlJH8AyyMOnkLHnHeP8Ff/DDpuZA/D3bPo=
|
||||||
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg=
|
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
|
||||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
|
||||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
|
||||||
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE=
|
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE=
|
||||||
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
|
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
|
||||||
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
|
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
|
||||||
@@ -524,7 +416,6 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
|
|||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
|
||||||
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||||
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||||
@@ -533,7 +424,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
|
|||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
@@ -559,13 +449,10 @@ github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfs
|
|||||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
|
||||||
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
|
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
|
||||||
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
|
||||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
|
||||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
@@ -598,20 +485,13 @@ github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ=
|
|||||||
github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
|
||||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
|
||||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||||
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
|
||||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
|
||||||
github.com/moby/buildkit v0.25.0 h1:cRgh74ymzyHxS5a/lsYT4OCyVU8iC3UgkwasIEUi0og=
|
github.com/moby/buildkit v0.25.0 h1:cRgh74ymzyHxS5a/lsYT4OCyVU8iC3UgkwasIEUi0og=
|
||||||
github.com/moby/buildkit v0.25.0/go.mod h1:phM8sdqnvgK2y1dPDnbwI6veUCXHOZ6KFSl6E164tkc=
|
github.com/moby/buildkit v0.25.0/go.mod h1:phM8sdqnvgK2y1dPDnbwI6veUCXHOZ6KFSl6E164tkc=
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
@@ -682,18 +562,10 @@ github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0ht
|
|||||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
|
||||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
|
||||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
|
|
||||||
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
|
|
||||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
|
||||||
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
|
|
||||||
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
|
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@@ -701,8 +573,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
|
||||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
|
||||||
github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
@@ -749,25 +619,16 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
|
||||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||||
github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=
|
|
||||||
github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=
|
|
||||||
github.com/secure-systems-lab/go-securesystemslib v0.6.0 h1:T65atpAVCJQK14UA57LMdZGpHi4QYSH/9FZyNGqMYIA=
|
github.com/secure-systems-lab/go-securesystemslib v0.6.0 h1:T65atpAVCJQK14UA57LMdZGpHi4QYSH/9FZyNGqMYIA=
|
||||||
github.com/secure-systems-lab/go-securesystemslib v0.6.0/go.mod h1:8Mtpo9JKks/qhPG4HGZ2LGMvrPbzuxwfz/f/zLfEWkk=
|
github.com/secure-systems-lab/go-securesystemslib v0.6.0/go.mod h1:8Mtpo9JKks/qhPG4HGZ2LGMvrPbzuxwfz/f/zLfEWkk=
|
||||||
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU=
|
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU=
|
||||||
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc=
|
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc=
|
||||||
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
|
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
|
||||||
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
|
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
|
||||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
|
||||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
|
||||||
github.com/siderolabs/grpc-proxy v0.5.1 h1:WTZYLMPTZPt43BzEJ02LT9kYA9qAfquWwCezc6NPPYE=
|
github.com/siderolabs/grpc-proxy v0.5.1 h1:WTZYLMPTZPt43BzEJ02LT9kYA9qAfquWwCezc6NPPYE=
|
||||||
github.com/siderolabs/grpc-proxy v0.5.1/go.mod h1:EQwE87LiWxhiIUPBeWmpjJb9DIWxWID8R6ARtdTC+8A=
|
github.com/siderolabs/grpc-proxy v0.5.1/go.mod h1:EQwE87LiWxhiIUPBeWmpjJb9DIWxWID8R6ARtdTC+8A=
|
||||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
@@ -777,53 +638,24 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w
|
|||||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||||
github.com/slackhq/nebula v1.10.3 h1:EstYj8ODEcv6T0R9X5BVq1zgWZnyU5gtPzk99QF1PMU=
|
|
||||||
github.com/slackhq/nebula v1.10.3/go.mod h1:IL5TUQm4x9IFx2kCKPYm1gP47pwd5b8QGnnBH2RHnvs=
|
|
||||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
|
|
||||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
|
|
||||||
github.com/smallstep/certificates v0.30.2 h1:1G3xBi8sJ740iA1mMPW2Svv7EIZKJ4Zf/iQtA5QlN0Y=
|
|
||||||
github.com/smallstep/certificates v0.30.2/go.mod h1:oyaE/aEYUGDr+YiCZLAxxP22bOQqcSHTeDgp8Vv2rlY=
|
|
||||||
github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=
|
|
||||||
github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y=
|
|
||||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
|
|
||||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
|
|
||||||
github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo=
|
|
||||||
github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc=
|
|
||||||
github.com/smallstep/nosql v0.8.0 h1:FBTCUfKPmWYbrozW+RBKu+fnvbn+zr5rVli/XB4Jp4A=
|
|
||||||
github.com/smallstep/nosql v0.8.0/go.mod h1:5dUpNotHLHhOUapP0PLBVVfp3tG1DFC31VRccg+Cqwo=
|
|
||||||
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
|
|
||||||
github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=
|
|
||||||
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA=
|
|
||||||
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492/go.mod h1:QQhwLqCS13nhv8L5ov7NgusowENUtXdEzdytjmJHdZQ=
|
|
||||||
github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4=
|
|
||||||
github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A=
|
|
||||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
|
||||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
|
||||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
|
||||||
github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk=
|
github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk=
|
||||||
github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE=
|
github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE=
|
||||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
|
||||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||||
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
|
||||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||||
github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
|
||||||
github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
|
github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
|
||||||
github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
|
github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@@ -831,7 +663,6 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
@@ -839,20 +670,13 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||||
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
|
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
|
||||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
|
||||||
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 h1:RnBbFMmodYzhC6adOjTbtUQXyzV8dcvKYbolzs6Qch0=
|
|
||||||
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747/go.mod h1:ejPAJui3kVK4u5TgMtqtXlWf5HnKh9fLy5kvpaeuas0=
|
|
||||||
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
|
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
|
||||||
github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw=
|
github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw=
|
||||||
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA=
|
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA=
|
||||||
@@ -867,11 +691,6 @@ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/
|
|||||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
|
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
|
||||||
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw=
|
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw=
|
||||||
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
|
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
|
||||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
|
||||||
github.com/unlabs-dev/bubbletea/v2 v2.0.8-uncloud.1 h1:898LVw5QvmRX/y+pgzCo2pE/NbN9CZfdpiVnzAeAFDY=
|
|
||||||
github.com/unlabs-dev/bubbletea/v2 v2.0.8-uncloud.1/go.mod h1:DGW2q8gvzHnOpMpZTORs0aySVHCox5C+2Svk0fci1qs=
|
|
||||||
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
|
|
||||||
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
|
|
||||||
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
|
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
|
||||||
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
||||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||||
@@ -884,7 +703,6 @@ github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8
|
|||||||
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
|
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
@@ -896,8 +714,6 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
|||||||
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
|
||||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
|
||||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
@@ -952,10 +768,6 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09
|
|||||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
go.step.sm/crypto v0.81.0 h1:e+ouzpNt3Xm4dp7HGXhgYB5y4iFik3vh3phHKWmvugU=
|
|
||||||
go.step.sm/crypto v0.81.0/go.mod h1:fsTizqQeASjTXnbv9O00XtRlIuXRkCdoRiJNyXGQujc=
|
|
||||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
|
||||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
@@ -974,7 +786,6 @@ go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
|||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
@@ -982,14 +793,8 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
|
||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
|
||||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8=
|
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
|
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||||
@@ -999,10 +804,6 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
|
|||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
|
||||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -1019,11 +820,6 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
|||||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
|
||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
|
||||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
@@ -1036,23 +832,15 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -1061,44 +849,24 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
|
||||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
|
||||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
|
||||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
|
||||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||||
@@ -1112,9 +880,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
|||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
|
||||||
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
|
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
|
||||||
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
|
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -1127,15 +892,11 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvY
|
|||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
||||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
google.golang.org/api v0.277.0 h1:HJfyJUiNeBBUMai7ez8u14wkp/gH/I4wpGbbO9o+cSk=
|
|
||||||
google.golang.org/api v0.277.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
|
|
||||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM=
|
||||||
@@ -1148,8 +909,6 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
|
|||||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||||
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
|
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
|
||||||
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
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-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 v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
@@ -1166,7 +925,6 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
|
|||||||
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
|
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
|
||||||
gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU=
|
gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
@@ -1181,7 +939,6 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
|||||||
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM=
|
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM=
|
||||||
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw=
|
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
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.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.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
@@ -1195,8 +952,6 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
|||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
|
||||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
|
||||||
k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
|
k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
|
||||||
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
|
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
|
||||||
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
|
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
|
||||||
|
|||||||
+1
-1
@@ -458,7 +458,7 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (_ *clie
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Snapshot the cluster store version so the new machine can catch up before participating.
|
// Snapshot the cluster store version so the new machine can catch up before participating.
|
||||||
var storeVersion map[string]uint64
|
var storeVersion api.StoreVersion
|
||||||
inspectResp, err = c.MachineClient.InspectMachine(ctx, &emptypb.Empty{})
|
inspectResp, err = c.MachineClient.InspectMachine(ctx, &emptypb.Empty{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO(lhf): remove Unimplemented check when v0.17.0 is released.
|
// TODO(lhf): remove Unimplemented check when v0.17.0 is released.
|
||||||
|
|||||||
+11
-8
@@ -45,7 +45,17 @@ func connectClusterWithProgress(ctx context.Context, conn config.MachineConnecti
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run the connection TUI model. Render to stderr so stdout stays clean for command output.
|
// Run the connection TUI model. Render to stderr so stdout stays clean for command output.
|
||||||
p := tea.NewProgram(newConnectModel(ctx, conn), tea.WithOutput(os.Stderr))
|
p := tea.NewProgram(newConnectModel(ctx, conn),
|
||||||
|
tea.WithOutput(os.Stderr),
|
||||||
|
tea.WithInput(nil),
|
||||||
|
// Ctrl+C handling
|
||||||
|
tea.WithFilter(func(_ tea.Model, msg tea.Msg) tea.Msg {
|
||||||
|
if _, ok := msg.(tea.InterruptMsg); ok {
|
||||||
|
return connectResultMsg{err: errors.New("connection cancelled")}
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}),
|
||||||
|
)
|
||||||
model, err := p.Run()
|
model, err := p.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("run connection TUI: %w", err)
|
return nil, fmt.Errorf("run connection TUI: %w", err)
|
||||||
@@ -190,13 +200,6 @@ func (m connectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
m.spinner, cmd = m.spinner.Update(msg)
|
m.spinner, cmd = m.spinner.Update(msg)
|
||||||
return m, cmd
|
return m, cmd
|
||||||
|
|
||||||
case tea.KeyPressMsg:
|
|
||||||
if msg.String() == "ctrl+c" {
|
|
||||||
m.result.err = fmt.Errorf("connection cancelled")
|
|
||||||
m.done = true
|
|
||||||
return m, tea.Quit
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package logs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Duration is an extended standard time.Duration that also supports days as a unit.
|
||||||
|
type Duration = time.Duration
|
||||||
|
|
||||||
|
// ParseDuration parses a Go duration with the additional unit d, meaning exactly 24 hours.
|
||||||
|
// Days can be fractional or combined with other units, such as "1.5d" or "2d3h".
|
||||||
|
func ParseDuration(value string) (Duration, error) {
|
||||||
|
if !strings.Contains(value, "d") {
|
||||||
|
return time.ParseDuration(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
rest := value
|
||||||
|
var normalised strings.Builder
|
||||||
|
if len(rest) > 0 && (rest[0] == '-' || rest[0] == '+') {
|
||||||
|
normalised.WriteByte(rest[0])
|
||||||
|
rest = rest[1:]
|
||||||
|
}
|
||||||
|
for len(rest) > 0 {
|
||||||
|
// Each component is a decimal number followed by a unit. Only the leading sign is allowed.
|
||||||
|
numberEnd := 0
|
||||||
|
for numberEnd < len(rest) && (isDigit(rest[numberEnd]) || rest[numberEnd] == '.') {
|
||||||
|
numberEnd++
|
||||||
|
}
|
||||||
|
unitEnd := numberEnd
|
||||||
|
for unitEnd < len(rest) && !isDigit(rest[unitEnd]) && rest[unitEnd] != '.' {
|
||||||
|
unitEnd++
|
||||||
|
}
|
||||||
|
if numberEnd == 0 || unitEnd == numberEnd {
|
||||||
|
return 0, fmt.Errorf("time: invalid duration '%s'", value)
|
||||||
|
}
|
||||||
|
number, unit := rest[:numberEnd], rest[numberEnd:unitEnd]
|
||||||
|
if unit == "d" {
|
||||||
|
// Use exact decimal arithmetic to avoid float rounding and retain nanosecond precision.
|
||||||
|
days, ok := new(big.Rat).SetString(number)
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("time: invalid duration '%s'", value)
|
||||||
|
}
|
||||||
|
days.Mul(days, new(big.Rat).SetInt64(int64(24*time.Hour)))
|
||||||
|
nanoseconds := new(big.Int).Quo(days.Num(), days.Denom())
|
||||||
|
normalised.WriteString(nanoseconds.String())
|
||||||
|
normalised.WriteString("ns")
|
||||||
|
} else {
|
||||||
|
normalised.WriteString(rest[:unitEnd])
|
||||||
|
}
|
||||||
|
rest = rest[unitEnd:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// The standard parser validates the remaining units and checks the total for overflow.
|
||||||
|
duration, err := time.ParseDuration(normalised.String())
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("time: invalid duration '%s': %w", value, err)
|
||||||
|
}
|
||||||
|
return duration, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDigit(c byte) bool {
|
||||||
|
return c >= '0' && c <= '9'
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package logs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseDuration(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, tt := range []struct {
|
||||||
|
input string
|
||||||
|
want time.Duration
|
||||||
|
}{
|
||||||
|
{"0", 0},
|
||||||
|
{"2m30s", 150 * time.Second},
|
||||||
|
{"1µs", time.Microsecond},
|
||||||
|
{"1μs", time.Microsecond},
|
||||||
|
{"2d", 48 * time.Hour},
|
||||||
|
{"010d", 240 * time.Hour},
|
||||||
|
{"08d", 192 * time.Hour},
|
||||||
|
{"2d3h", 51 * time.Hour},
|
||||||
|
{"3h2d", 51 * time.Hour},
|
||||||
|
{"1d1d", 48 * time.Hour},
|
||||||
|
{"1.5d", 36 * time.Hour},
|
||||||
|
{".5d", 12 * time.Hour},
|
||||||
|
{"1.d", 24 * time.Hour},
|
||||||
|
{"-2d3h", -51 * time.Hour},
|
||||||
|
{"+2d", 48 * time.Hour},
|
||||||
|
{"0d", 0},
|
||||||
|
{"0d1ns", time.Nanosecond},
|
||||||
|
{"0.000000000001d", 86 * time.Nanosecond},
|
||||||
|
{"-0.000000000001d", -86 * time.Nanosecond},
|
||||||
|
{"106751d23h47m16.854775807s", time.Duration(math.MaxInt64)},
|
||||||
|
{"-106751d23h47m16.854775808s", time.Duration(math.MinInt64)},
|
||||||
|
} {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
actual, err := ParseDuration(tt.input)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseDuration_Invalid(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, input := range []string{
|
||||||
|
"", "d", "2d3", "1..2d", ".d", "1d-2h", "1d+2h", "1d 2h", "1day", "2D", "1w",
|
||||||
|
"1e2d", "1d2w", "106752d", "-106752d", "106751d23h47m16.854775808s",
|
||||||
|
"-106751d23h47m16.854775809s", "999999999999999999999999999d",
|
||||||
|
} {
|
||||||
|
t.Run(input, func(t *testing.T) {
|
||||||
|
_, err := ParseDuration(input)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
"github.com/charmbracelet/colorprofile"
|
||||||
"github.com/docker/docker/pkg/stringid"
|
"github.com/docker/docker/pkg/stringid"
|
||||||
"github.com/psviderski/uncloud/internal/cli/tui"
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
@@ -24,6 +25,10 @@ type Formatter struct {
|
|||||||
maxServiceWidth int
|
maxServiceWidth int
|
||||||
|
|
||||||
utc bool
|
utc bool
|
||||||
|
|
||||||
|
// Cache each stream's terminal profile to avoid detection on every log entry.
|
||||||
|
stdout *colorprofile.Writer
|
||||||
|
stderr *colorprofile.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFormatter(machineNames, serviceNames []string, utc bool) *Formatter {
|
func NewFormatter(machineNames, serviceNames []string, utc bool) *Formatter {
|
||||||
@@ -50,6 +55,8 @@ func NewFormatter(machineNames, serviceNames []string, utc bool) *Formatter {
|
|||||||
maxMachineWidth: maxMachineWidth,
|
maxMachineWidth: maxMachineWidth,
|
||||||
maxServiceWidth: maxServiceWidth,
|
maxServiceWidth: maxServiceWidth,
|
||||||
utc: utc,
|
utc: utc,
|
||||||
|
stdout: colorprofile.NewWriter(os.Stdout, os.Environ()),
|
||||||
|
stderr: colorprofile.NewWriter(os.Stderr, os.Environ()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,9 +145,9 @@ func (f *Formatter) PrintEntry(entry api.ServiceLogEntry) {
|
|||||||
|
|
||||||
// Print to appropriate stream.
|
// Print to appropriate stream.
|
||||||
if entry.Stream == api.LogStreamStderr {
|
if entry.Stream == api.LogStreamStderr {
|
||||||
fmt.Fprint(os.Stderr, output.String())
|
fmt.Fprint(f.stderr, output.String())
|
||||||
} else {
|
} else {
|
||||||
fmt.Print(output.String())
|
fmt.Fprint(f.stdout, output.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,8 +155,7 @@ func (f *Formatter) PrintEntry(entry api.ServiceLogEntry) {
|
|||||||
func (f *Formatter) printError(entry api.ServiceLogEntry) {
|
func (f *Formatter) printError(entry api.ServiceLogEntry) {
|
||||||
if entry.Metadata.ServiceName == "" {
|
if entry.Metadata.ServiceName == "" {
|
||||||
msg := fmt.Sprintf("ERROR: %v", entry.Err)
|
msg := fmt.Sprintf("ERROR: %v", entry.Err)
|
||||||
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.BrightRed)
|
fmt.Fprintln(f.stderr, tui.BoldRed.Render(msg))
|
||||||
fmt.Fprintln(os.Stderr, style.Render(msg))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,8 +177,7 @@ func (f *Formatter) printError(entry api.ServiceLogEntry) {
|
|||||||
msg += fmt.Sprintf(": %v", entry.Err)
|
msg += fmt.Sprintf(": %v", entry.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.BrightYellow)
|
fmt.Fprintln(f.stderr, tui.BoldYellow.Render(msg))
|
||||||
fmt.Fprintln(os.Stderr, style.Render(msg))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// palette is available colors for machine/service differentiation.
|
// palette is available colors for machine/service differentiation.
|
||||||
|
|||||||
@@ -19,6 +19,18 @@ type Options struct {
|
|||||||
Machines []string
|
Machines []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TailLines resolves the default tail limit after parsing flags. An empty opts.Tail means the user
|
||||||
|
// did not specify a limit, so --since can select all matching logs without overriding an explicit --tail.
|
||||||
|
func (opts Options) TailLines() (int, error) {
|
||||||
|
if opts.Tail == "" {
|
||||||
|
if opts.Since != "" {
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
|
return 100, nil
|
||||||
|
}
|
||||||
|
return ParseTail(opts.Tail)
|
||||||
|
}
|
||||||
|
|
||||||
func Flags(options *Options) *pflag.FlagSet {
|
func Flags(options *Options) *pflag.FlagSet {
|
||||||
set := &pflag.FlagSet{}
|
set := &pflag.FlagSet{}
|
||||||
|
|
||||||
@@ -29,14 +41,17 @@ func Flags(options *Options) *pflag.FlagSet {
|
|||||||
set.StringVar(&options.Since, "since", "",
|
set.StringVar(&options.Since, "since", "",
|
||||||
"Show logs generated on or after the given timestamp. Accepts relative duration, RFC 3339 date, or Unix timestamp.\n"+
|
"Show logs generated on or after the given timestamp. Accepts relative duration, RFC 3339 date, or Unix timestamp.\n"+
|
||||||
"Examples:\n"+
|
"Examples:\n"+
|
||||||
" --since 2m30s Relative duration (2 minutes 30 seconds ago)\n"+
|
" --since 1h45m Relative duration (1 hour 45 minutes ago)\n"+
|
||||||
" --since 1h Relative duration (1 hour ago)\n"+
|
" Supported units: d (day = 24h), h (hour), m (minute),\n"+
|
||||||
" --since 2025-11-24 RFC 3339 date only (midnight using local timezone)\n"+
|
" s (second), ms (millisecond),\n"+
|
||||||
" --since 2024-05-14T22:50:00 RFC 3339 date/time using local timezone\n"+
|
" us/µs (microsecond), ns (nanosecond)\n"+
|
||||||
|
" --since 2025-11-24 RFC 3339 date only (midnight using client local timezone)\n"+
|
||||||
|
" --since 2024-05-14T22:50:00 RFC 3339 date/time using client local timezone\n"+
|
||||||
" --since 2024-01-31T10:30:00Z RFC 3339 date/time in UTC\n"+
|
" --since 2024-01-31T10:30:00Z RFC 3339 date/time in UTC\n"+
|
||||||
" --since 1763953966 Unix timestamp (seconds since January 1, 1970)")
|
" --since 1763953966 Unix timestamp (seconds since January 1, 1970)")
|
||||||
set.StringVarP(&options.Tail, "tail", "n", "100",
|
set.StringVarP(&options.Tail, "tail", "n", "",
|
||||||
"Show the most recent logs and limit the number of lines shown per replica. Use 'all' to show all logs.")
|
"Show the most recent logs and limit the number of lines shown per replica. Use 'all' to show all logs.\n"+
|
||||||
|
"Defaults to 100, or 'all' when --since is set.")
|
||||||
set.StringVar(&options.Until, "until", "",
|
set.StringVar(&options.Until, "until", "",
|
||||||
"Show logs generated before the given timestamp. Accepts relative duration, RFC 3339 date, or Unix timestamp.\n"+
|
"Show logs generated before the given timestamp. Accepts relative duration, RFC 3339 date, or Unix timestamp.\n"+
|
||||||
"See --since for examples.")
|
"See --since for examples.")
|
||||||
@@ -46,7 +61,7 @@ func Flags(options *Options) *pflag.FlagSet {
|
|||||||
return set
|
return set
|
||||||
}
|
}
|
||||||
|
|
||||||
func Tail(tail string) (int, error) {
|
func ParseTail(tail string) (int, error) {
|
||||||
if tail == "all" {
|
if tail == "all" {
|
||||||
return -1, nil
|
return -1, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,42 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestOptionsTailLines(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"default", nil, 100},
|
||||||
|
{"follow", []string{"-f"}, 100},
|
||||||
|
{"since", []string{"--since", "1h"}, -1},
|
||||||
|
{"since and follow", []string{"--since", "1h", "-f"}, -1},
|
||||||
|
{"until only", []string{"--until", "1h"}, 100},
|
||||||
|
{"time range", []string{"--since", "3h", "--until", "1h"}, -1},
|
||||||
|
{"explicit default with since", []string{"--since", "1h", "--tail", "100"}, 100},
|
||||||
|
{"explicit limit with since", []string{"--since", "1h", "-n", "20"}, 20},
|
||||||
|
{"explicit limit before since", []string{"-n", "20", "--since", "1h"}, 20},
|
||||||
|
{"explicit all", []string{"--tail", "all"}, -1},
|
||||||
|
{"explicit zero with since", []string{"--since", "1h", "-n", "0", "-f"}, 0},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var options Options
|
||||||
|
require.NoError(t, Flags(&options).Parse(tt.args))
|
||||||
|
tail, err := options.TailLines()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want, tail)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var options Options
|
||||||
|
require.NoError(t, Flags(&options).Parse([]string{"--since", "1h", "--tail", "invalid"}))
|
||||||
|
_, err := options.TailLines()
|
||||||
|
require.ErrorContains(t, err, "invalid --tail value")
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseServiceArgs(t *testing.T) {
|
func TestParseServiceArgs(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package logs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TimeRange resolves log filters that could be RFC 3339, timestamp, or relative duration to UTC timestamps.
|
||||||
|
// Dates without a timezone use now's location. Relative durations are computed from now.
|
||||||
|
func TimeRange(since, until string, now time.Time) (string, string, error) {
|
||||||
|
var err error
|
||||||
|
since, err = timestamp(since, now)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("invalid --since value: %w", err)
|
||||||
|
}
|
||||||
|
until, err = timestamp(until, now)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("invalid --until value: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return since, until, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func timestamp(value string, now time.Time) (string, error) {
|
||||||
|
if value == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
// A bare zero is the Unix epoch, matching Docker's log filters.
|
||||||
|
if duration, err := ParseDuration(value); value != "0" && err == nil {
|
||||||
|
return now.Add(-duration).UTC().Format(time.RFC3339Nano), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep Docker's supported date layouts, but use the location's offset at the requested date.
|
||||||
|
for _, layout := range []string{
|
||||||
|
time.RFC3339Nano,
|
||||||
|
"2006-01-02T15:04Z07:00",
|
||||||
|
"2006-01-02T15Z07:00",
|
||||||
|
"2006-01-02Z07:00",
|
||||||
|
"2006-01-02T15:04:05.999999999",
|
||||||
|
"2006-01-02T15:04",
|
||||||
|
"2006-01-02T15",
|
||||||
|
"2006-01-02",
|
||||||
|
} {
|
||||||
|
if t, err := time.ParseInLocation(layout, value, now.Location()); err == nil {
|
||||||
|
return t.UTC().Format(time.RFC3339Nano), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seconds, fraction, hasFraction := strings.Cut(value, ".")
|
||||||
|
sec, err := strconv.ParseInt(seconds, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to parse '%s' as a time or duration", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
var nsec int64
|
||||||
|
if hasFraction {
|
||||||
|
if len(fraction) == 0 || len(fraction) > 9 || strings.ContainsAny(fraction, "+-") {
|
||||||
|
return "", fmt.Errorf("invalid Unix timestamp fraction in '%s'", value)
|
||||||
|
}
|
||||||
|
nsec, err = strconv.ParseInt(fraction+strings.Repeat("0", 9-len(fraction)), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid Unix timestamp fraction in '%s': %w", value, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t := time.Unix(sec, nsec).UTC()
|
||||||
|
if t.Year() < 0 || t.Year() > 9999 {
|
||||||
|
return "", fmt.Errorf("Unix timestamp '%s' is outside the RFC 3339 date range", value)
|
||||||
|
}
|
||||||
|
return t.Format(time.RFC3339Nano), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package logs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
timetypes "github.com/docker/docker/api/types/time"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTimeRange(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
location, err := time.LoadLocation("Australia/Sydney")
|
||||||
|
require.NoError(t, err)
|
||||||
|
// October is daylight-saving time, but July timestamps must use the winter offset.
|
||||||
|
// Daylight saving starts on 4 October. Day durations must still mean 24 elapsed hours.
|
||||||
|
now := time.Date(2026, 10, 5, 12, 0, 0, 123456789, location)
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"2026-07-01", "2026-06-30T14:00:00Z"},
|
||||||
|
{"2026-07-01T10", "2026-07-01T00:00:00Z"},
|
||||||
|
{"2026-07-01T10:30", "2026-07-01T00:30:00Z"},
|
||||||
|
{"2026-07-01T10:30:45.123456789", "2026-07-01T00:30:45.123456789Z"},
|
||||||
|
{"2026-01-01T10:00:00", "2025-12-31T23:00:00Z"},
|
||||||
|
{"2026-07-01T10:30:45Z", "2026-07-01T10:30:45Z"},
|
||||||
|
{"2026-07-01T10:30:45+02:00", "2026-07-01T08:30:45Z"},
|
||||||
|
{"2026-07-01T10:30:45-04:00", "2026-07-01T14:30:45Z"},
|
||||||
|
{"2026-07-01T10+02:00", "2026-07-01T08:00:00Z"},
|
||||||
|
{"2026-07-01T10:30+02:00", "2026-07-01T08:30:00Z"},
|
||||||
|
{"2026-07-01+02:00", "2026-06-30T22:00:00Z"},
|
||||||
|
{"1763953966", "2025-11-24T03:12:46Z"},
|
||||||
|
{"1763953966.000000001", "2025-11-24T03:12:46.000000001Z"},
|
||||||
|
{"0", "1970-01-01T00:00:00Z"},
|
||||||
|
{"2m30s", "2026-10-05T00:57:30.123456789Z"},
|
||||||
|
{"1d", "2026-10-04T01:00:00.123456789Z"},
|
||||||
|
{"2d", "2026-10-03T01:00:00.123456789Z"},
|
||||||
|
{"2d3h", "2026-10-02T22:00:00.123456789Z"},
|
||||||
|
{"1.5d", "2026-10-03T13:00:00.123456789Z"},
|
||||||
|
{"-2d", "2026-10-07T01:00:00.123456789Z"},
|
||||||
|
{"-1h", "2026-10-05T02:00:00.123456789Z"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
since, until, err := TimeRange(tt.input, tt.input, now)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want, since)
|
||||||
|
assert.Equal(t, tt.want, until)
|
||||||
|
|
||||||
|
if since != "" {
|
||||||
|
// The server-side Docker SDK must preserve the cutoff even in another timezone.
|
||||||
|
actual, err := timetypes.GetTimestamp(since, now.In(time.UTC))
|
||||||
|
require.NoError(t, err)
|
||||||
|
expected, err := time.Parse(time.RFC3339Nano, tt.want)
|
||||||
|
require.NoError(t, err)
|
||||||
|
sec, nsec, err := timetypes.ParseTimestamps(actual, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, expected.Equal(time.Unix(sec, nsec)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
since, until, err := TimeRange("3h", "1h30m", now)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "2026-10-04T22:00:00.123456789Z", since)
|
||||||
|
assert.Equal(t, "2026-10-04T23:30:00.123456789Z", until)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeRange_Invalid(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, input := range []string{
|
||||||
|
"invalid", "2026-02-30", "2026-01-01T25:00:00", "1763953966.xyz",
|
||||||
|
"1763953966.1234567890", "253402300800",
|
||||||
|
} {
|
||||||
|
t.Run(input, func(t *testing.T) {
|
||||||
|
_, _, err := TimeRange(input, "", time.Now())
|
||||||
|
require.ErrorContains(t, err, "invalid --since value")
|
||||||
|
_, _, err = TimeRange("", input, time.Now())
|
||||||
|
require.ErrorContains(t, err, "invalid --until value")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
type eventIDKey struct{}
|
type eventIDKey struct{}
|
||||||
|
|
||||||
// WithEventID returns a context that overrides the default event ID used by client methods.
|
// WithEventID returns a context that overrides the event ID already set in the context.
|
||||||
func WithEventID(ctx context.Context, eventID string) context.Context {
|
func WithEventID(ctx context.Context, eventID string) context.Context {
|
||||||
return context.WithValue(ctx, eventIDKey{}, eventID)
|
return context.WithValue(ctx, eventIDKey{}, eventID)
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ func OldPreDeployHookEventID(serviceName, containerID, machineName string) strin
|
|||||||
tui.Faint.Render(" on ") + machineName
|
tui.Faint.Render(" on ") + machineName
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageEventID returns a progress event ID for image pull operations.
|
// ImageEventID returns a progress event ID for image operations on a machine.
|
||||||
func ImageEventID(image, machineName string) string {
|
func ImageEventID(image, machineName string) string {
|
||||||
return tui.Faint.Render("Image ") + image +
|
return tui.Faint.Render("Image ") + image +
|
||||||
tui.Faint.Render(" on ") + machineName
|
tui.Faint.Render(" on ") + machineName
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package tui
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PrintWarning(msg string) {
|
func PrintWarning(msg string) {
|
||||||
styledMsg := BoldYellow.Render(fmt.Sprintf("WARNING: %s", msg))
|
styledMsg := BoldYellow.Render(fmt.Sprintf("WARNING: %s", msg))
|
||||||
fmt.Fprintln(os.Stderr, styledMsg)
|
lipgloss.Fprintln(os.Stderr, styledMsg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ func RunSpinner(ctx context.Context, title string, action func(ctx context.Conte
|
|||||||
}
|
}
|
||||||
})).
|
})).
|
||||||
WithOutput(os.Stderr).
|
WithOutput(os.Stderr).
|
||||||
|
// A spinner reads no keys, saying so makes Bubble Tea skip unnecessary terminal capability queries.
|
||||||
|
WithInput(nil).
|
||||||
Context(ctx).
|
Context(ctx).
|
||||||
ActionWithErr(action).
|
ActionWithErr(action).
|
||||||
Run()
|
Run()
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const adminCommandTimeout = 5 * time.Second
|
||||||
|
|
||||||
// AdminClient is a client for the Corrosion admin API.
|
// AdminClient is a client for the Corrosion admin API.
|
||||||
type AdminClient struct {
|
type AdminClient struct {
|
||||||
sockPath string
|
sockPath string
|
||||||
@@ -32,10 +34,15 @@ type Response struct {
|
|||||||
// The channel will be closed after sending the last or error response. The caller must read from the channel until
|
// The channel will be closed after sending the last or error response. The caller must read from the channel until
|
||||||
// it is closed.
|
// it is closed.
|
||||||
func (c *AdminClient) SendCommand(cmd []byte) (<-chan Response, error) {
|
func (c *AdminClient) SendCommand(cmd []byte) (<-chan Response, error) {
|
||||||
conn, err := net.Dial("unix", c.sockPath)
|
// TODO: Accept contexts in admin methods so callers can cancel requests and set their deadlines.
|
||||||
|
conn, err := net.DialTimeout("unix", c.sockPath, adminCommandTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("connect to admin socket: %w", err)
|
return nil, fmt.Errorf("connect to admin socket: %w", err)
|
||||||
}
|
}
|
||||||
|
if err = conn.SetDeadline(time.Now().Add(adminCommandTimeout)); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, fmt.Errorf("set admin connection deadline: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if _, err = conn.Write(encodeFrame(cmd)); err != nil {
|
if _, err = conn.Write(encodeFrame(cmd)); err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
)
|
)
|
||||||
@@ -31,11 +32,11 @@ func logs(ctx context.Context, unit string, opts api.ServiceLogsOptions) (io.Rea
|
|||||||
|
|
||||||
if opts.Since != "" {
|
if opts.Since != "" {
|
||||||
args = append(args, "-S")
|
args = append(args, "-S")
|
||||||
args = append(args, opts.Since)
|
args = append(args, journalTimestamp(opts.Since))
|
||||||
}
|
}
|
||||||
if opts.Until != "" {
|
if opts.Until != "" {
|
||||||
args = append(args, "-U")
|
args = append(args, "-U")
|
||||||
args = append(args, opts.Until)
|
args = append(args, journalTimestamp(opts.Until))
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := commandContext(ctx, journalctl, args...)
|
cmd := commandContext(ctx, journalctl, args...)
|
||||||
@@ -51,6 +52,18 @@ func logs(ctx context.Context, unit string, opts api.ServiceLogsOptions) (io.Rea
|
|||||||
return p, cmd.Wait, nil
|
return p, cmd.Wait, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// journalTimestamp formats normalised client timestamps for journalctl versions that do not
|
||||||
|
// accept RFC 3339 timezone suffixes. Support for timestamps containing T and Z was added in
|
||||||
|
// systemd 255 (6 December 2023). Debian 12 ships systemd 252, so it still needs this conversion.
|
||||||
|
// Journald timestamps have microsecond precision.
|
||||||
|
func journalTimestamp(value string) string {
|
||||||
|
if t, err := time.Parse(time.RFC3339Nano, value); err == nil {
|
||||||
|
return t.UTC().Format("2006-01-02 15:04:05.999999") + " UTC"
|
||||||
|
}
|
||||||
|
// Preserve raw filters from older clients and SDK callers.
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
// follow synchronously follows the io.Reader, writing each new journal entry to channel.
|
// follow synchronously follows the io.Reader, writing each new journal entry to channel.
|
||||||
// It stops when the reader is exhausted or the context is cancelled.
|
// It stops when the reader is exhausted or the context is cancelled.
|
||||||
func follow(ctx context.Context, reader io.Reader, outCh chan api.LogEntry) {
|
func follow(ctx context.Context, reader io.Reader, outCh chan api.LogEntry) {
|
||||||
|
|||||||
@@ -115,6 +115,9 @@ func TestEntry(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLogs(t *testing.T) {
|
func TestLogs(t *testing.T) {
|
||||||
|
originalCommandContext := commandContext
|
||||||
|
t.Cleanup(func() { commandContext = originalCommandContext })
|
||||||
|
|
||||||
commandContext = func(ctx context.Context, _ string, _ ...string) *exec.Cmd {
|
commandContext = func(ctx context.Context, _ string, _ ...string) *exec.Cmd {
|
||||||
return exec.CommandContext(ctx, "/usr/bin/tail", "testdata/logs")
|
return exec.CommandContext(ctx, "/usr/bin/tail", "testdata/logs")
|
||||||
}
|
}
|
||||||
@@ -150,3 +153,38 @@ func TestLogs(t *testing.T) {
|
|||||||
// Still six because heartbeats are not written here and Tail is ignored as the command is overridden.
|
// Still six because heartbeats are not written here and Tail is ignored as the command is overridden.
|
||||||
assert.Equal(t, 6, i)
|
assert.Equal(t, 6, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLogs_TimeFilters(t *testing.T) {
|
||||||
|
originalCommandContext := commandContext
|
||||||
|
t.Cleanup(func() { commandContext = originalCommandContext })
|
||||||
|
|
||||||
|
var args []string
|
||||||
|
commandContext = func(ctx context.Context, command string, commandArgs ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "journalctl", command)
|
||||||
|
args = commandArgs
|
||||||
|
return exec.CommandContext(ctx, "/usr/bin/tail", "testdata/logs")
|
||||||
|
}
|
||||||
|
|
||||||
|
ch, err := Logs(context.Background(), "uncloud", api.ServiceLogsOptions{
|
||||||
|
Since: "2026-07-01T10:30:45.123456789+10:00",
|
||||||
|
Until: "2026-07-01T01:30:45Z",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
for range ch {
|
||||||
|
}
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"-u", "uncloud", "--no-hostname", "-n", "0", "-o", "short-unix",
|
||||||
|
"-S", "2026-07-01 00:30:45.123456 UTC",
|
||||||
|
"-U", "2026-07-01 01:30:45 UTC",
|
||||||
|
}, args)
|
||||||
|
|
||||||
|
// Older clients and SDK callers can still pass journalctl's native filters.
|
||||||
|
ch, err = Logs(context.Background(), "uncloud", api.ServiceLogsOptions{Since: "1h ago", Until: "today"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
for range ch {
|
||||||
|
}
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"-u", "uncloud", "--no-hostname", "-n", "0", "-o", "short-unix",
|
||||||
|
"-S", "1h ago", "-U", "today",
|
||||||
|
}, args)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"cmp"
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"maps"
|
"maps"
|
||||||
@@ -57,14 +58,12 @@ https://{{$hostname}} {
|
|||||||
log
|
log
|
||||||
}{{end}}
|
}{{end}}
|
||||||
`
|
`
|
||||||
caddyfileUnavailabeFooter = `# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
bootstrapMarker = "# Uncloud bootstrap for Caddy container "
|
||||||
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
|
legacyBootstrapMarker = "# NOTE: User-defined configs for services were skipped because Caddy is not running"
|
||||||
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
|
|
||||||
# and its logs for more details (uc logs caddy).
|
|
||||||
`
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CaddyfileGenerator generates a Caddyfile configuration for the Caddy reverse proxy.
|
// CaddyfileGenerator generates a Caddyfile configuration for the Caddy reverse proxy.
|
||||||
|
// It combines generated routes from published ports with user-defined Caddyfile snippets from service specs (x-caddy).
|
||||||
type CaddyfileGenerator struct {
|
type CaddyfileGenerator struct {
|
||||||
// machineID is the unique identifier of the machine where the controller is running.
|
// machineID is the unique identifier of the machine where the controller is running.
|
||||||
machineID string
|
machineID string
|
||||||
@@ -74,11 +73,18 @@ type CaddyfileGenerator struct {
|
|||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// CaddyfileValidator is an interface for validating Caddyfile configurations.
|
// CaddyfileValidator checks a candidate Caddyfile without loading it. Validate returns InvalidCaddyfileError only
|
||||||
|
// when the candidate is known to be invalid. Other errors mean the check could not be completed. A successful check
|
||||||
|
// does not guarantee that the configuration will load.
|
||||||
type CaddyfileValidator interface {
|
type CaddyfileValidator interface {
|
||||||
Validate(ctx context.Context, caddyfile string) error
|
Validate(ctx context.Context, caddyfile string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InvalidCaddyfileError means validation completed and the candidate Caddyfile was rejected.
|
||||||
|
type InvalidCaddyfileError struct{ Message string }
|
||||||
|
|
||||||
|
func (e *InvalidCaddyfileError) Error() string { return e.Message }
|
||||||
|
|
||||||
func NewCaddyfileGenerator(
|
func NewCaddyfileGenerator(
|
||||||
machineID, machineName string, validator CaddyfileValidator, log *slog.Logger,
|
machineID, machineName string, validator CaddyfileValidator, log *slog.Logger,
|
||||||
) *CaddyfileGenerator {
|
) *CaddyfileGenerator {
|
||||||
@@ -93,35 +99,41 @@ func NewCaddyfileGenerator(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate creates a Caddyfile configuration based on the provided service containers.
|
// Generate creates a Caddyfile for the local Caddy container from the supplied healthy application containers.
|
||||||
// The Caddyfile is generated from the service ports of the healthy containers.
|
// Application service ports provide the generated sites. The Caddy container's custom Caddy config (x-caddy), if
|
||||||
// If a 'caddy' service container is running on this machine and defines a custom Caddy config (x-caddy) in its service
|
// defined, provides the global block even when that container is unhealthy. When application custom Caddy configs
|
||||||
// spec, it will be validated and prepended to the generated Caddyfile. Custom Caddy configs (x-caddy) defined in other
|
// are included, the newest container for each service provides its config.
|
||||||
// service specs are validated and appended to the generated Caddyfile. Invalid configs are logged and skipped to ensure
|
|
||||||
// the generated Caddyfile remains valid.
|
|
||||||
//
|
//
|
||||||
// The final Caddyfile structure includes:
|
// The resulting Caddyfile has this structure:
|
||||||
//
|
//
|
||||||
// [caddy x-caddy (global config)]
|
// [caddy custom Caddy config (global)]
|
||||||
// [generated Caddyfile from all service ports]
|
// [generated sites from application service ports]
|
||||||
// [service-a x-caddy]
|
// [service-a custom Caddy config]
|
||||||
// ...
|
// ...
|
||||||
// [service-z x-caddy]
|
// [service-z custom Caddy config]
|
||||||
//
|
//
|
||||||
// If includeCustom is false, custom Caddy configs (x-caddy) are not included in the generated Caddyfile.
|
// Bootstrap mode keeps the global custom Caddy config and generated sites, but omits application custom Caddy configs.
|
||||||
|
// It skips validation because Caddy may not be running yet. In this case, Caddy validates the config when it starts.
|
||||||
|
// Global template errors still stop generation. In full mode, invalid globals stop generation, while invalid
|
||||||
|
// application custom Caddy configs are logged and skipped.
|
||||||
func (g *CaddyfileGenerator) Generate(
|
func (g *CaddyfileGenerator) Generate(
|
||||||
ctx context.Context, records []store.ContainerRecord, includeCustom bool,
|
ctx context.Context,
|
||||||
|
caddyCtr api.ServiceContainer,
|
||||||
|
records []store.ContainerRecord,
|
||||||
|
bootstrap bool,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
|
records = slices.Clone(records)
|
||||||
// Sort records by local machine first, then by service name and creation time. Placing containers on the local
|
// Sort records by local machine first, then by service name and creation time. Placing containers on the local
|
||||||
// machine first lets user-defined Caddy configs pair this ordering with the "first" lb_policy to always send
|
// machine first lets user-defined Caddy configs pair this ordering with the "first" lb_policy to always send
|
||||||
// traffic to the same-host replica (skipping the cross-machine hop) and only fall back to remote upstreams when
|
// traffic to the same-host replica (skipping the cross-machine hop) and only fall back to remote upstreams when
|
||||||
// the local one is unhealthy.
|
// the local one is unhealthy.
|
||||||
// The service name and creation time tiebreakers keep the generated Caddyfile stable across regenerations.
|
// The service name, creation time, and container ID tiebreakers keep the file stable across regenerations.
|
||||||
slices.SortStableFunc(records, func(a, b store.ContainerRecord) int {
|
slices.SortStableFunc(records, func(a, b store.ContainerRecord) int {
|
||||||
return cmp.Or(
|
return cmp.Or(
|
||||||
g.localMachineRank(a.MachineID)-g.localMachineRank(b.MachineID),
|
g.localMachineRank(a.MachineID)-g.localMachineRank(b.MachineID),
|
||||||
strings.Compare(a.Container.ServiceName(), b.Container.ServiceName()),
|
strings.Compare(a.Container.ServiceName(), b.Container.ServiceName()),
|
||||||
a.Container.CreatedTime().Compare(b.Container.CreatedTime()),
|
a.Container.CreatedTime().Compare(b.Container.CreatedTime()),
|
||||||
|
strings.Compare(a.Container.ID, b.Container.ID),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -136,26 +148,11 @@ func (g *CaddyfileGenerator) Generate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
caddyfileHeader := fmt.Sprintf(caddyfileHeaderFmt, g.machineName, time.Now().UTC().Format(time.RFC3339))
|
caddyfileHeader := fmt.Sprintf(caddyfileHeaderFmt, g.machineName, time.Now().UTC().Format(time.RFC3339))
|
||||||
if !includeCustom {
|
|
||||||
return fmt.Sprintf("%s\n%s\n%s", caddyfileHeader, caddyfile, caddyfileUnavailabeFooter), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
upstreams := serviceUpstreams(containers)
|
upstreams := serviceUpstreams(containers)
|
||||||
// Track validation errors for reporting.
|
// Track validation errors for reporting.
|
||||||
var configErrors []string
|
var configErrors []string
|
||||||
|
|
||||||
// Find the 'caddy' service container on this machine. Use the most recent one if multiple exist.
|
if caddyCtr.ServiceSpec.CaddyConfig() != "" {
|
||||||
var caddyCtr *api.ServiceContainer
|
|
||||||
for _, cr := range records {
|
|
||||||
if cr.MachineID == g.machineID && cr.Container.ServiceName() == CaddyServiceName &&
|
|
||||||
(caddyCtr == nil || cr.Container.CreatedTime().Compare(caddyCtr.CreatedTime()) > 0) {
|
|
||||||
caddyCtr = &cr.Container
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the caddy container is running on this machine and has a custom Caddy config (global),
|
|
||||||
// prepend it to the generated Caddyfile and validate it.
|
|
||||||
if caddyCtr != nil && caddyCtr.ServiceSpec.CaddyConfig() != "" {
|
|
||||||
// Render the custom global Caddy config as a Go template with the upstreams.
|
// Render the custom global Caddy config as a Go template with the upstreams.
|
||||||
tmplCtx := templateContext{
|
tmplCtx := templateContext{
|
||||||
Name: caddyCtr.ServiceName(),
|
Name: caddyCtr.ServiceName(),
|
||||||
@@ -163,23 +160,23 @@ func (g *CaddyfileGenerator) Generate(
|
|||||||
}
|
}
|
||||||
renderedConfig, err := renderCaddyfile(tmplCtx, caddyCtr.ServiceSpec.CaddyConfig())
|
renderedConfig, err := renderCaddyfile(tmplCtx, caddyCtr.ServiceSpec.CaddyConfig())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.log.Error("Failed to render template directives in user-defined global Caddy config, skipping it.",
|
return "", fmt.Errorf(
|
||||||
"service", caddyCtr.ServiceName(), "container", caddyCtr.ID, "err", err)
|
"render template directives in user-defined global Caddy config from Caddy container %s: %w",
|
||||||
configErrors = append(configErrors,
|
caddyCtr.ShortID(), err)
|
||||||
fmt.Sprintf("service '%s': failed to render template: %v", caddyCtr.ServiceName(), err))
|
}
|
||||||
} else {
|
|
||||||
caddyfileCandidate := fmt.Sprintf("# User-defined global config from service '%s'.\n%s\n\n%s",
|
caddyfileCandidate := fmt.Sprintf("# User-defined global config from service '%s'.\n%s\n\n%s",
|
||||||
caddyCtr.ServiceName(), renderedConfig, caddyfile)
|
caddyCtr.ServiceName(), renderedConfig, caddyfile)
|
||||||
|
if !bootstrap && g.validator != nil {
|
||||||
if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil {
|
if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil {
|
||||||
g.log.Error("User-defined global Caddy config is invalid, skipping it.",
|
return "", fmt.Errorf("validate user-defined global Caddy config from Caddy container %s: %w",
|
||||||
"service", caddyCtr.ServiceName(), "container", caddyCtr.ID, "err", err)
|
caddyCtr.ShortID(), err)
|
||||||
configErrors = append(configErrors,
|
}
|
||||||
fmt.Sprintf("service '%s': validation failed: %v", caddyCtr.ServiceName(), err))
|
}
|
||||||
} else {
|
|
||||||
caddyfile = caddyfileCandidate
|
caddyfile = caddyfileCandidate
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if bootstrap {
|
||||||
|
return caddyfileHeader + bootstrapMarker + caddyCtr.ID + "\n\n" + caddyfile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// There could be multiple service containers for the same service with different custom Caddy configs, for example,
|
// There could be multiple service containers for the same service with different custom Caddy configs, for example,
|
||||||
@@ -200,7 +197,7 @@ func (g *CaddyfileGenerator) Generate(
|
|||||||
// Append a custom Caddy config for each service to the Caddyfile and validate it. If the config for a service
|
// Append a custom Caddy config for each service to the Caddyfile and validate it. If the config for a service
|
||||||
// is invalid, skip it but continue processing other services to ensure the Caddyfile remains valid.
|
// is invalid, skip it but continue processing other services to ensure the Caddyfile remains valid.
|
||||||
for _, serviceName := range sortedServiceNames {
|
for _, serviceName := range sortedServiceNames {
|
||||||
// Skip the caddy container as we already processed it.
|
// Skip the caddy container as we already processed it as the global config.
|
||||||
if serviceName == CaddyServiceName {
|
if serviceName == CaddyServiceName {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -226,16 +223,22 @@ func (g *CaddyfileGenerator) Generate(
|
|||||||
|
|
||||||
caddyfileCandidate := fmt.Sprintf("%s\n# User-defined config for service '%s'.\n%s\n",
|
caddyfileCandidate := fmt.Sprintf("%s\n# User-defined config for service '%s'.\n%s\n",
|
||||||
caddyfile, serviceName, renderedConfig)
|
caddyfile, serviceName, renderedConfig)
|
||||||
|
if g.validator != nil {
|
||||||
if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil {
|
if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil {
|
||||||
|
if _, ok := errors.AsType[*InvalidCaddyfileError](err); !ok {
|
||||||
|
return "", fmt.Errorf("validate Caddy config for service '%s': %w", serviceName, err)
|
||||||
|
}
|
||||||
g.log.Error("User-defined Caddy config for service is invalid, skipping it.",
|
g.log.Error("User-defined Caddy config for service is invalid, skipping it.",
|
||||||
"service", serviceName, "err", err)
|
"service", serviceName, "err", err)
|
||||||
configErrors = append(configErrors, fmt.Sprintf("service '%s': validation failed: %v", serviceName, err))
|
configErrors = append(configErrors,
|
||||||
} else {
|
fmt.Sprintf("service '%s': validation failed: %v", serviceName, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
caddyfile = caddyfileCandidate
|
caddyfile = caddyfileCandidate
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Append error summary as comment if there were any invalid configs.
|
// Keep skipped-snippet errors in the saved file so an operator can see why routes are missing after a restart.
|
||||||
if len(configErrors) > 0 {
|
if len(configErrors) > 0 {
|
||||||
var errorsComment strings.Builder
|
var errorsComment strings.Builder
|
||||||
errorsComment.WriteString("# Skipped invalid user-defined configs:\n")
|
errorsComment.WriteString("# Skipped invalid user-defined configs:\n")
|
||||||
@@ -249,8 +252,7 @@ func (g *CaddyfileGenerator) Generate(
|
|||||||
return caddyfileHeader + "\n" + caddyfile, nil
|
return caddyfileHeader + "\n" + caddyfile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// localMachineRank returns 0 if the given machineID matches the local machine and 1 otherwise.
|
// localMachineRank is a sorting function for containers that puts running on the local machine first.
|
||||||
// Useful for sorting containers running locally first.
|
|
||||||
func (g *CaddyfileGenerator) localMachineRank(machineID string) int {
|
func (g *CaddyfileGenerator) localMachineRank(machineID string) int {
|
||||||
if g.machineID == machineID {
|
if g.machineID == machineID {
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -208,10 +208,11 @@ http://app.example.com {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Validator is not expected to be called in these tests.
|
validator := NewMockCaddyfileValidator(t)
|
||||||
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", nil, nil)
|
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
|
||||||
|
|
||||||
config, err := generator.Generate(ctx, tt.containers, true)
|
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.1", "", "", time.Now()).Container
|
||||||
|
config, err := generator.Generate(ctx, caddyCtr, tt.containers, false)
|
||||||
|
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -332,7 +333,7 @@ bad.template.com {
|
|||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "caddy service with invalid global config is skipped",
|
name: "caddy service with invalid global config aborts",
|
||||||
containers: []store.ContainerRecord{
|
containers: []store.ContainerRecord{
|
||||||
newContainerRecordWithCaddyConfig(
|
newContainerRecordWithCaddyConfig(
|
||||||
"caddy",
|
"caddy",
|
||||||
@@ -345,10 +346,7 @@ localhost {
|
|||||||
time.Now(),
|
time.Now(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
want: testCaddyfileHeader + `
|
wantErr: true,
|
||||||
# Skipped invalid user-defined configs:
|
|
||||||
# - service 'caddy': validation failed: invalid config detected
|
|
||||||
`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "caddy service on different machine is ignored",
|
name: "caddy service on different machine is ignored",
|
||||||
@@ -786,7 +784,7 @@ web-v2.example.com {
|
|||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple errors: invalid global, template error, and validation error",
|
name: "invalid global aborts before application configs",
|
||||||
containers: []store.ContainerRecord{
|
containers: []store.ContainerRecord{
|
||||||
newContainerRecordWithCaddyConfig(
|
newContainerRecordWithCaddyConfig(
|
||||||
"caddy",
|
"caddy",
|
||||||
@@ -827,17 +825,7 @@ invalid.example.com {
|
|||||||
time.Now(),
|
time.Now(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
want: testCaddyfileHeader + `
|
wantErr: true,
|
||||||
# User-defined config for service 'valid'.
|
|
||||||
valid.example.com {
|
|
||||||
respond "Valid config"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Skipped invalid user-defined configs:
|
|
||||||
# - service 'caddy': validation failed: invalid config detected
|
|
||||||
# - service 'broken-template': failed to render template: parse config as Go template: template: Caddyfile:2: unterminated quoted string
|
|
||||||
# - service 'invalid': validation failed: invalid config detected
|
|
||||||
`,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -846,7 +834,7 @@ valid.example.com {
|
|||||||
validator.EXPECT().Validate(mock.Anything, mock.Anything).RunAndReturn(
|
validator.EXPECT().Validate(mock.Anything, mock.Anything).RunAndReturn(
|
||||||
func(ctx context.Context, caddyfile string) error {
|
func(ctx context.Context, caddyfile string) error {
|
||||||
if strings.Contains(caddyfile, "# test:invalid") {
|
if strings.Contains(caddyfile, "# test:invalid") {
|
||||||
return errors.New("invalid config detected")
|
return &InvalidCaddyfileError{Message: "invalid config detected"}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -855,7 +843,8 @@ valid.example.com {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
|
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
|
||||||
|
|
||||||
config, err := generator.Generate(ctx, tt.containers, true)
|
caddyCtr := caddyContainerFromTestContainers(tt.containers)
|
||||||
|
config, err := generator.Generate(ctx, caddyCtr, tt.containers, false)
|
||||||
|
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -868,6 +857,167 @@ valid.example.com {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCaddyfileGeneratorBootstrap(t *testing.T) {
|
||||||
|
// Bootstrap keeps the local Caddy container's globals and generated routes, but omits application custom configs.
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
containers []store.ContainerRecord
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "global config retained and application custom configs skipped",
|
||||||
|
containers: []store.ContainerRecord{
|
||||||
|
newContainerRecordWithCaddyConfig(
|
||||||
|
"caddy",
|
||||||
|
"10.210.0.1",
|
||||||
|
`# Global config
|
||||||
|
{
|
||||||
|
global directive
|
||||||
|
}`,
|
||||||
|
"test-machine-id",
|
||||||
|
time.Now(),
|
||||||
|
),
|
||||||
|
newContainerRecordWithCaddyConfig(
|
||||||
|
"web",
|
||||||
|
"10.210.0.2",
|
||||||
|
`web.example.com {
|
||||||
|
reverse_proxy web:3000
|
||||||
|
}`,
|
||||||
|
"test-machine-id",
|
||||||
|
time.Now(),
|
||||||
|
),
|
||||||
|
newContainerRecordWithPorts(
|
||||||
|
"api",
|
||||||
|
"10.210.0.3",
|
||||||
|
[]string{"api.example.com:8080/http"},
|
||||||
|
"test-machine-id",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
want: strings.Replace(testCaddyfileHeader, "\n\n# Health check endpoint", `
|
||||||
|
# Uncloud bootstrap for Caddy container caddy-10.210.0.1
|
||||||
|
|
||||||
|
# User-defined global config from service 'caddy'.
|
||||||
|
# Global config
|
||||||
|
{
|
||||||
|
global directive
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint`, 1) + `
|
||||||
|
# Sites generated from service ports.
|
||||||
|
|
||||||
|
http://api.example.com {
|
||||||
|
reverse_proxy 10.210.0.3:8080 {
|
||||||
|
import common_proxy
|
||||||
|
}
|
||||||
|
log
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no containers with x-caddy configs",
|
||||||
|
containers: []store.ContainerRecord{
|
||||||
|
newContainerRecordWithPorts(
|
||||||
|
"api",
|
||||||
|
"10.210.0.3",
|
||||||
|
[]string{"api.example.com:8080/http"},
|
||||||
|
"test-machine-id",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
want: strings.Replace(testCaddyfileHeader, "\n\n# Health check endpoint", `
|
||||||
|
# Uncloud bootstrap for Caddy container caddy-10.210.0.1
|
||||||
|
|
||||||
|
# Health check endpoint`, 1) + `
|
||||||
|
# Sites generated from service ports.
|
||||||
|
|
||||||
|
http://api.example.com {
|
||||||
|
reverse_proxy 10.210.0.3:8080 {
|
||||||
|
import common_proxy
|
||||||
|
}
|
||||||
|
log
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
validator := NewMockCaddyfileValidator(t)
|
||||||
|
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
|
||||||
|
|
||||||
|
caddyContainer := caddyContainerFromTestContainers(tt.containers)
|
||||||
|
config, err := generator.Generate(ctx, caddyContainer, tt.containers, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCaddyfileGenerator_ValidatorTransportErrorAbortsGeneration(t *testing.T) {
|
||||||
|
validator := NewMockCaddyfileValidator(t)
|
||||||
|
validator.EXPECT().Validate(mock.Anything, mock.Anything).Return(errors.New("socket disappeared"))
|
||||||
|
|
||||||
|
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
|
||||||
|
app := newContainerRecordWithCaddyConfig(
|
||||||
|
"web",
|
||||||
|
"10.210.0.2",
|
||||||
|
"web.example.com { respond ok }",
|
||||||
|
"test-machine-id",
|
||||||
|
time.Now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := generator.Generate(context.Background(), caddyContainerFromTestContainers(nil),
|
||||||
|
[]store.ContainerRecord{app}, false)
|
||||||
|
require.ErrorContains(t, err, "socket disappeared")
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContainer(ip string, ports ...string) api.ServiceContainer {
|
||||||
|
portsLabel := strings.Join(ports, ",")
|
||||||
|
return api.ServiceContainer{Container: api.Container{InspectResponse: container.InspectResponse{
|
||||||
|
ContainerJSONBase: &container.ContainerJSONBase{
|
||||||
|
State: &container.State{
|
||||||
|
Running: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NetworkSettings: &container.NetworkSettings{
|
||||||
|
Networks: map[string]*network.EndpointSettings{
|
||||||
|
docker.NetworkName: {
|
||||||
|
IPAddress: ip,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: &container.Config{
|
||||||
|
Labels: map[string]string{
|
||||||
|
api.LabelServicePorts: portsLabel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
|
||||||
|
portsLabel := strings.Join(ports, ",")
|
||||||
|
return api.ServiceContainer{Container: api.Container{InspectResponse: container.InspectResponse{
|
||||||
|
ContainerJSONBase: &container.ContainerJSONBase{
|
||||||
|
State: &container.State{
|
||||||
|
Running: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NetworkSettings: &container.NetworkSettings{
|
||||||
|
Networks: map[string]*network.EndpointSettings{
|
||||||
|
"other-network": {
|
||||||
|
IPAddress: "172.17.0.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: &container.Config{
|
||||||
|
Labels: map[string]string{
|
||||||
|
api.LabelServicePorts: portsLabel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}}
|
||||||
|
}
|
||||||
|
|
||||||
func newContainerRecord(ctr api.ServiceContainer, machineID string) store.ContainerRecord {
|
func newContainerRecord(ctr api.ServiceContainer, machineID string) store.ContainerRecord {
|
||||||
return store.ContainerRecord{
|
return store.ContainerRecord{
|
||||||
Container: ctr,
|
Container: ctr,
|
||||||
@@ -911,100 +1061,6 @@ func newContainerRecordWithCaddyConfig(serviceName, ip, caddyConfig, machineID s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCaddyfileGeneratorWithoutCustomConfigs(t *testing.T) {
|
|
||||||
// Test that when includeCustom is false (Caddy not available), x-caddy configs are skipped.
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
containers []store.ContainerRecord
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "x-caddy configs are skipped",
|
|
||||||
containers: []store.ContainerRecord{
|
|
||||||
newContainerRecordWithCaddyConfig(
|
|
||||||
"caddy",
|
|
||||||
"10.210.0.1",
|
|
||||||
`# Global config
|
|
||||||
{
|
|
||||||
global directive
|
|
||||||
}`,
|
|
||||||
"test-machine-id",
|
|
||||||
time.Now(),
|
|
||||||
),
|
|
||||||
newContainerRecordWithCaddyConfig(
|
|
||||||
"web",
|
|
||||||
"10.210.0.2",
|
|
||||||
`web.example.com {
|
|
||||||
reverse_proxy web:3000
|
|
||||||
}`,
|
|
||||||
"test-machine-id",
|
|
||||||
time.Now(),
|
|
||||||
),
|
|
||||||
newContainerRecordWithPorts(
|
|
||||||
"api",
|
|
||||||
"10.210.0.3",
|
|
||||||
[]string{"api.example.com:8080/http"},
|
|
||||||
"test-machine-id",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
want: testCaddyfileHeader + `
|
|
||||||
# Sites generated from service ports.
|
|
||||||
|
|
||||||
http://api.example.com {
|
|
||||||
reverse_proxy 10.210.0.3:8080 {
|
|
||||||
import common_proxy
|
|
||||||
}
|
|
||||||
log
|
|
||||||
}
|
|
||||||
|
|
||||||
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
|
||||||
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
|
|
||||||
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
|
|
||||||
# and its logs for more details (uc logs caddy).
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no containers with x-caddy configs",
|
|
||||||
containers: []store.ContainerRecord{
|
|
||||||
newContainerRecordWithPorts(
|
|
||||||
"api",
|
|
||||||
"10.210.0.3",
|
|
||||||
[]string{"api.example.com:8080/http"},
|
|
||||||
"test-machine-id",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
want: testCaddyfileHeader + `
|
|
||||||
# Sites generated from service ports.
|
|
||||||
|
|
||||||
http://api.example.com {
|
|
||||||
reverse_proxy 10.210.0.3:8080 {
|
|
||||||
import common_proxy
|
|
||||||
}
|
|
||||||
log
|
|
||||||
}
|
|
||||||
|
|
||||||
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
|
||||||
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
|
|
||||||
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
|
|
||||||
# and its logs for more details (uc logs caddy).
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Validator is not expected to be called in these tests.
|
|
||||||
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", nil, nil)
|
|
||||||
|
|
||||||
config, err := generator.Generate(ctx, tt.containers, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newContainerRecordWithPorts(serviceName, ip string, ports []string, machineID string) store.ContainerRecord {
|
func newContainerRecordWithPorts(serviceName, ip string, ports []string, machineID string) store.ContainerRecord {
|
||||||
portsLabel := strings.Join(ports, ",")
|
portsLabel := strings.Join(ports, ",")
|
||||||
return store.ContainerRecord{
|
return store.ContainerRecord{
|
||||||
@@ -1037,3 +1093,18 @@ func newContainerRecordWithPorts(serviceName, ip string, ports []string, machine
|
|||||||
MachineID: machineID,
|
MachineID: machineID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// caddyContainerFromTestContainers returns the local Caddy container from the provided test containers,
|
||||||
|
// or creates a default one if not found.
|
||||||
|
func caddyContainerFromTestContainers(records []store.ContainerRecord) api.ServiceContainer {
|
||||||
|
if caddyCtr := selectLocalCaddyContainer(records, "test-machine-id"); caddyCtr != nil {
|
||||||
|
return *caddyCtr
|
||||||
|
}
|
||||||
|
return newContainerRecordWithCaddyConfig(
|
||||||
|
"caddy",
|
||||||
|
"10.210.0.1",
|
||||||
|
"",
|
||||||
|
"test-machine-id",
|
||||||
|
time.Now(),
|
||||||
|
).Container
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package caddyconfig
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@@ -26,15 +25,15 @@ func NewCaddyAdminClient(socketPath string) *CaddyAdminClient {
|
|||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||||
return net.Dial("unix", socketPath)
|
return (&net.Dialer{}).DialContext(ctx, "unix", socketPath)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAvailable checks if the local Caddy instance is listening on the admin socket.
|
// IsAvailable checks whether a Caddy process accepts connections at the admin socket.
|
||||||
func (c *CaddyAdminClient) IsAvailable() bool {
|
func (c *CaddyAdminClient) IsAvailable() bool {
|
||||||
conn, err := net.DialTimeout("unix", c.socketPath, 1*time.Second)
|
conn, err := net.DialTimeout("unix", c.socketPath, 1*time.Second)
|
||||||
// A stale socket file left over from a crashed Caddy container returns ECONNREFUSED so this is correctly handled
|
// A stale socket file left over from a crashed Caddy container returns ECONNREFUSED so this is correctly handled
|
||||||
@@ -80,15 +79,16 @@ func (c *CaddyAdminClient) Adapt(ctx context.Context, caddyfile string) (string,
|
|||||||
// If the response is a 400 Bad Request, try to parse the error message from it.
|
// If the response is a 400 Bad Request, try to parse the error message from it.
|
||||||
if resp.StatusCode == http.StatusBadRequest {
|
if resp.StatusCode == http.StatusBadRequest {
|
||||||
var apiError caddy.APIError
|
var apiError caddy.APIError
|
||||||
if err = json.Unmarshal(body, &apiError); err == nil {
|
if err = json.Unmarshal(body, &apiError); err == nil && apiError.Message != "" {
|
||||||
return "", errors.New(apiError.Message)
|
return "", &InvalidCaddyfileError{Message: apiError.Message}
|
||||||
}
|
}
|
||||||
|
return "", &InvalidCaddyfileError{Message: string(body)}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", errors.New(string(body))
|
return "", fmt.Errorf("adapt request failed: HTTP %d: %s", resp.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads a Caddyfile configuration into the Caddy instance running on the machine.
|
// Load loads a Caddyfile configuration into the Caddy instance.
|
||||||
// Due to a Caddy bug (https://github.com/caddyserver/caddy/issues/7246), we first adapt the Caddyfile to JSON
|
// Due to a Caddy bug (https://github.com/caddyserver/caddy/issues/7246), we first adapt the Caddyfile to JSON
|
||||||
// and then load the JSON config to get proper error handling.
|
// and then load the JSON config to get proper error handling.
|
||||||
func (c *CaddyAdminClient) Load(ctx context.Context, caddyfile string) error {
|
func (c *CaddyAdminClient) Load(ctx context.Context, caddyfile string) error {
|
||||||
@@ -96,7 +96,11 @@ func (c *CaddyAdminClient) Load(ctx context.Context, caddyfile string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("adapt Caddyfile to JSON config: %w", err)
|
return fmt.Errorf("adapt Caddyfile to JSON config: %w", err)
|
||||||
}
|
}
|
||||||
|
return c.LoadJSON(ctx, jsonConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadJSON loads a JSON configuration into the Caddy instance.
|
||||||
|
func (c *CaddyAdminClient) LoadJSON(ctx context.Context, jsonConfig string) error {
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", "http://localhost/load", strings.NewReader(jsonConfig))
|
req, err := http.NewRequestWithContext(ctx, "POST", "http://localhost/load", strings.NewReader(jsonConfig))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create load request: %w", err)
|
return fmt.Errorf("create load request: %w", err)
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package caddyconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testAdminServer(t *testing.T, handler http.HandlerFunc) string {
|
||||||
|
t.Helper()
|
||||||
|
// macOS limits Unix socket paths to 104 bytes. t.TempDir can exceed that.
|
||||||
|
dir, err := os.MkdirTemp("/tmp", "uc-caddy-")
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = os.RemoveAll(dir)
|
||||||
|
})
|
||||||
|
|
||||||
|
socketPath := filepath.Join(dir, "admin.sock")
|
||||||
|
listener, err := net.Listen("unix", socketPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
server := httptest.NewUnstartedServer(handler)
|
||||||
|
_ = server.Listener.Close()
|
||||||
|
server.Listener = listener
|
||||||
|
server.Start()
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
|
return socketPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCaddyAdminClient_Validate(t *testing.T) {
|
||||||
|
var status atomic.Int64
|
||||||
|
status.Store(http.StatusBadRequest)
|
||||||
|
socket := testAdminServer(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(int(status.Load()))
|
||||||
|
_, _ = w.Write([]byte(`{"message":"invalid config"}`))
|
||||||
|
})
|
||||||
|
client := NewCaddyAdminClient(socket)
|
||||||
|
err := client.Validate(context.Background(), "invalid")
|
||||||
|
var invalid *InvalidCaddyfileError
|
||||||
|
assert.ErrorAs(t, err, &invalid)
|
||||||
|
|
||||||
|
status.Store(http.StatusInternalServerError)
|
||||||
|
err = client.Validate(context.Background(), "invalid")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.NotErrorAs(t, err, &invalid)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ package caddyconfig
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/fs"
|
"github.com/psviderski/uncloud/internal/fs"
|
||||||
"github.com/psviderski/uncloud/internal/machine/store"
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
@@ -20,27 +21,40 @@ const (
|
|||||||
CaddyServiceName = "caddy"
|
CaddyServiceName = "caddy"
|
||||||
CaddyGroup = "uncloud"
|
CaddyGroup = "uncloud"
|
||||||
VerifyPath = "/.uncloud-verify"
|
VerifyPath = "/.uncloud-verify"
|
||||||
|
// periodicReconciliationInterval is the interval at which the controller reconciles the Caddyfile
|
||||||
|
// even if no container changes are observed.
|
||||||
|
periodicReconciliationInterval = 30 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// Controller monitors container changes in the cluster store and generates a configuration file for Caddy reverse
|
// Controller keeps the local Caddy reverse proxy in sync with container state from the cluster store. It writes a
|
||||||
// proxy. The generated configuration allows Caddy to route external traffic to service containers across the internal
|
// Caddyfile that routes external traffic to healthy service containers across the internal network.
|
||||||
// network.
|
//
|
||||||
|
// Current Caddy deployments share one Caddyfile and one admin socket per machine. The controller must preserve a
|
||||||
|
// saved full Caddyfile when Caddy is unavailable because that file may be needed to restart or roll back a container.
|
||||||
|
// It writes a reduced bootstrap config only when no full file exists, including when an older offline file must be
|
||||||
|
// replaced. The shared layout cannot give overlapping Caddy revisions separate configurations.
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
machineID string
|
machineID string
|
||||||
caddyfilePath string
|
caddyfilePath string
|
||||||
|
service *Service
|
||||||
generator *CaddyfileGenerator
|
generator *CaddyfileGenerator
|
||||||
client *CaddyAdminClient
|
client *CaddyAdminClient
|
||||||
store *store.Store
|
store *store.Store
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
// lastFingerprint caches the fingerprint of the containers used to generate the latest successfully loaded
|
// Tests use their own group so they can exercise real file publication without the daemon's uncloud group.
|
||||||
// Caddyfile. nil means it hasn't been loaded yet or the last load failed.
|
fileGroup string
|
||||||
|
// lastFingerprint records the healthy application-container inputs used for the last full Caddyfile that was
|
||||||
|
// successfully loaded into Caddy and saved to disk. It excludes the Caddy container, whose identity, start time,
|
||||||
|
// and global config are tracked separately below.
|
||||||
lastFingerprint []containerFingerprint
|
lastFingerprint []containerFingerprint
|
||||||
// lastCaddyfile caches the last generated Caddyfile.
|
lastCaddyCtrID string
|
||||||
lastCaddyfile string
|
lastStartedAt string
|
||||||
|
lastGlobal string
|
||||||
|
lastSavedBody string
|
||||||
}
|
}
|
||||||
|
|
||||||
// containerFingerprint is the subset of container data that the Caddyfile generator depends on.
|
// containerFingerprint contains container identity and routing inputs that can change the generated Caddyfile.
|
||||||
// Comparing fingerprints lets the controller skip no-op regenerations.
|
// It excludes incidental Docker state so unrelated container updates do not cause a Caddy reload.
|
||||||
type containerFingerprint struct {
|
type containerFingerprint struct {
|
||||||
ID string
|
ID string
|
||||||
IP netip.Addr
|
IP netip.Addr
|
||||||
@@ -56,7 +70,8 @@ func (f containerFingerprint) Equal(other containerFingerprint) bool {
|
|||||||
f.CaddyConfig == other.CaddyConfig
|
f.CaddyConfig == other.CaddyConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewController(machineID, configDir, adminSock string, store *store.Store) (*Controller, error) {
|
func NewController(machineID string, service *Service, adminSock string, store *store.Store) (*Controller, error) {
|
||||||
|
configDir := service.configDir
|
||||||
if err := os.MkdirAll(configDir, 0o750); err != nil {
|
if err := os.MkdirAll(configDir, 0o750); err != nil {
|
||||||
return nil, fmt.Errorf("create directory for Caddy configuration '%s': %w", configDir, err)
|
return nil, fmt.Errorf("create directory for Caddy configuration '%s': %w", configDir, err)
|
||||||
}
|
}
|
||||||
@@ -67,16 +82,21 @@ func NewController(machineID, configDir, adminSock string, store *store.Store) (
|
|||||||
log := slog.With("component", "caddy-controller")
|
log := slog.With("component", "caddy-controller")
|
||||||
client := NewCaddyAdminClient(adminSock)
|
client := NewCaddyAdminClient(adminSock)
|
||||||
|
|
||||||
// generator is initialised by Run() once the machine name is resolved from the store.
|
// The generator is initialised by Run() after resolving the machine name from the store.
|
||||||
return &Controller{
|
return &Controller{
|
||||||
machineID: machineID,
|
machineID: machineID,
|
||||||
caddyfilePath: filepath.Join(configDir, "Caddyfile"),
|
caddyfilePath: filepath.Join(configDir, "Caddyfile"),
|
||||||
|
service: service,
|
||||||
client: client,
|
client: client,
|
||||||
store: store,
|
store: store,
|
||||||
log: log,
|
log: log,
|
||||||
|
fileGroup: CaddyGroup,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run reconciles on container changes and every periodicReconciliationInterval. Failed work is retried every second,
|
||||||
|
// but repeated retry failures are logged only during the periodic check.
|
||||||
|
// The periodic check also covers missed events and outstanding failures after the daemon restarts.
|
||||||
func (c *Controller) Run(ctx context.Context) error {
|
func (c *Controller) Run(ctx context.Context) error {
|
||||||
// Default the machine name to the machine ID so the Caddyfile header still carries a stable identifier if
|
// Default the machine name to the machine ID so the Caddyfile header still carries a stable identifier if
|
||||||
// the store lookup fails.
|
// the store lookup fails.
|
||||||
@@ -95,13 +115,34 @@ func (c *Controller) Run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
c.log.Info("Subscribed to container changes in the cluster to generate Caddy configuration.")
|
c.log.Info("Subscribed to container changes in the cluster to generate Caddy configuration.")
|
||||||
|
|
||||||
containers = filterHealthyContainers(containers)
|
var retryC <-chan time.Time
|
||||||
c.generateAndLoadCaddyfile(ctx, containers)
|
periodic := time.NewTicker(periodicReconciliationInterval)
|
||||||
|
defer periodic.Stop()
|
||||||
|
|
||||||
// TODO: left for backward compatibility, remove later.
|
handleResult := func(err error, logFailure bool) {
|
||||||
if err = c.generateJSONConfig(containers); err != nil {
|
c.service.setReconciliationResult(err)
|
||||||
c.log.Error("Failed to generate Caddy JSON configuration to disk.", "err", err)
|
if err == nil {
|
||||||
|
retryC = nil
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
if logFailure {
|
||||||
|
c.log.Error("Failed to reconcile Caddy configuration, will retry.", "err", err)
|
||||||
|
}
|
||||||
|
retryC = time.After(time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
reconcile := func(logFailure bool) {
|
||||||
|
ctrs, err := c.store.ListContainers(ctx, store.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("list containers: %w", err)
|
||||||
|
} else {
|
||||||
|
err = c.generateAndLoadCaddyfile(ctx, ctrs)
|
||||||
|
}
|
||||||
|
handleResult(err, logFailure)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The subscription supplies an initial snapshot, so the first attempt need not wait for a change event.
|
||||||
|
handleResult(c.generateAndLoadCaddyfile(ctx, containers), true)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -111,25 +152,18 @@ func (c *Controller) Run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
c.log.Debug("Cluster containers changed, regenerating Caddy configuration.")
|
c.log.Debug("Cluster containers changed, regenerating Caddy configuration.")
|
||||||
|
|
||||||
containers, err = c.store.ListContainers(ctx, store.ListOptions{})
|
reconcile(true)
|
||||||
if err != nil {
|
case <-periodic.C:
|
||||||
c.log.Error("Failed to list containers.", "err", err)
|
reconcile(true)
|
||||||
continue
|
case <-retryC:
|
||||||
}
|
reconcile(false)
|
||||||
containers = filterHealthyContainers(containers)
|
|
||||||
c.generateAndLoadCaddyfile(ctx, containers)
|
|
||||||
|
|
||||||
// TODO: left for backward compatibility, remove later.
|
|
||||||
if err = c.generateJSONConfig(containers); err != nil {
|
|
||||||
c.log.Error("Failed to generate Caddy JSON configuration to disk.", "err", err)
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterHealthyContainers filters out unhealthy and hook containers.
|
// filterHealthyContainers filters out unhealthy, hook, and caddy containers.
|
||||||
// TODO: Filters out containers from this machine that are likely unavailable. The availability can be determined
|
// TODO: Filters out containers from this machine that are likely unavailable. The availability can be determined
|
||||||
// by the cluster membership state of the machine that the container is running on. Implement machine membership
|
// by the cluster membership state of the machine that the container is running on. Implement machine membership
|
||||||
// check using Corrossion Admin client.
|
// check using Corrossion Admin client.
|
||||||
@@ -139,6 +173,9 @@ func filterHealthyContainers(containers []store.ContainerRecord) []store.Contain
|
|||||||
if cr.Container.IsHook() {
|
if cr.Container.IsHook() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if cr.Container.ServiceName() == CaddyServiceName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if cr.Container.Healthy() {
|
if cr.Container.Healthy() {
|
||||||
healthy = append(healthy, cr)
|
healthy = append(healthy, cr)
|
||||||
}
|
}
|
||||||
@@ -146,65 +183,96 @@ func filterHealthyContainers(containers []store.ContainerRecord) []store.Contain
|
|||||||
return healthy
|
return healthy
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateAndLoadCaddyfile regenerates the Caddyfile from the given containers and loads it into the local Caddy
|
// generateAndLoadCaddyfile reconciles the shared Caddyfile for the selected local Caddy container. A full candidate
|
||||||
// if available.
|
// reaches disk only after Caddy accepts it. Without an admin endpoint, the controller keeps a saved full file intact
|
||||||
func (c *Controller) generateAndLoadCaddyfile(ctx context.Context, containers []store.ContainerRecord) {
|
// and writes a bootstrap config only when the file is absent or already a bootstrap.
|
||||||
// Check if Caddy is available before attempting to generate and load config.
|
func (c *Controller) generateAndLoadCaddyfile(ctx context.Context, containers []store.ContainerRecord) error {
|
||||||
caddyAvailable := c.client.IsAvailable()
|
caddyCtr := selectLocalCaddyContainer(containers, c.machineID)
|
||||||
|
if caddyCtr == nil {
|
||||||
// Skip regeneration when Caddy is available and the containers since the last successful load haven't changed.
|
// Caddy is not running locally which means there is no reliable source for the global config, skipping.
|
||||||
// When Caddy is unavailable we still regenerate to keep the Caddyfile on disk updated.
|
return nil
|
||||||
fingerprint := fingerprintContainers(containers)
|
|
||||||
if caddyAvailable && slices.EqualFunc(fingerprint, c.lastFingerprint, containerFingerprint.Equal) {
|
|
||||||
c.log.Debug("Caddy configuration is unchanged.", "path", c.caddyfilePath)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
caddyfile, err := c.generator.Generate(ctx, containers, caddyAvailable)
|
saved, readErr := os.ReadFile(c.caddyfilePath)
|
||||||
|
if readErr != nil && !errors.Is(readErr, os.ErrNotExist) {
|
||||||
|
return fmt.Errorf("read saved Caddyfile: %w", readErr)
|
||||||
|
}
|
||||||
|
haveSaved := readErr == nil
|
||||||
|
|
||||||
|
healthyCtrs := filterHealthyContainers(containers)
|
||||||
|
fingerprint := fingerprintContainers(healthyCtrs)
|
||||||
|
|
||||||
|
if !caddyCtr.State.Running || !c.client.IsAvailable() {
|
||||||
|
if haveSaved && !isBootstrapCaddyfile(string(saved)) {
|
||||||
|
// Caddy may need this file to restart or roll back. A hosts-only replacement could drop
|
||||||
|
// storage or other global settings while the admin API is unavailable.
|
||||||
|
if caddyCtr.State.Running {
|
||||||
|
return fmt.Errorf("caddy admin socket unavailable, preserving saved Caddyfile")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap, err := c.generator.Generate(ctx, *caddyCtr, healthyCtrs, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.log.Error("Failed to generate Caddyfile configuration.", "err", err)
|
return fmt.Errorf("generate Caddy bootstrap config: %w", err)
|
||||||
return
|
}
|
||||||
|
if err = c.writeCaddyfileIfChanged(bootstrap); err != nil {
|
||||||
|
return fmt.Errorf("save Caddy bootstrap config: %w", err)
|
||||||
|
}
|
||||||
|
if caddyCtr.State.Running {
|
||||||
|
return fmt.Errorf("caddy admin socket unavailable, bootstrap config saved")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !caddyAvailable {
|
if haveSaved &&
|
||||||
// Caddy is not running so the generated Caddyfile should not include user-defined configs thus must be valid.
|
caddyfileBody(string(saved)) == c.lastSavedBody &&
|
||||||
// It's safe to write the config to disk so that when Caddy is deployed on this machine, it can pick it up.
|
!isBootstrapCaddyfile(string(saved)) &&
|
||||||
if err = c.writeCaddyfileIfChanged(caddyfile); err != nil {
|
c.lastCaddyCtrID == caddyCtr.ID &&
|
||||||
c.log.Error("Failed to write Caddyfile to disk.", "err", err)
|
c.lastStartedAt == caddyCtr.State.StartedAt &&
|
||||||
return
|
c.lastGlobal == caddyCtr.ServiceSpec.CaddyConfig() &&
|
||||||
}
|
slices.EqualFunc(fingerprint, c.lastFingerprint, containerFingerprint.Equal) {
|
||||||
c.log.Debug("Caddy is not running on this machine, skipping configuration load.", "path", c.caddyfilePath)
|
// No changes to the inputs that affect the generated Caddyfile, so no reload is needed.
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caddy is available, try to load the config which may fail if the config is invalid. Generally, a config can
|
caddyfile, err := c.generator.Generate(ctx, *caddyCtr, healthyCtrs, false)
|
||||||
// pass the adaptation/validation step but still fail to load, for example, if it references resources that are
|
if err != nil {
|
||||||
// not available.
|
return fmt.Errorf("generate Caddyfile: %w", err)
|
||||||
|
}
|
||||||
|
// Try to load the config which may fail if the config is invalid. Generally, a config can pass
|
||||||
|
// the adaptation/validation step but still fail to load, for example, if it references resources
|
||||||
|
// that are not available.
|
||||||
if err = c.client.Load(ctx, caddyfile); err != nil {
|
if err = c.client.Load(ctx, caddyfile); err != nil {
|
||||||
c.log.Error("Failed to load new Caddy configuration into local Caddy instance.",
|
return fmt.Errorf("load Caddyfile: %w", err)
|
||||||
"err", err, "path", c.caddyfilePath)
|
}
|
||||||
// Mark the cache stale so the next container change retries the load even if the container set is unchanged.
|
if err = c.writeCaddyfileIfChanged(caddyfile); err != nil {
|
||||||
c.lastFingerprint = nil
|
return fmt.Errorf("save loaded Caddyfile: %w", err)
|
||||||
// Don't write invalid config to disk.
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.lastFingerprint = fingerprint
|
c.lastFingerprint = fingerprint
|
||||||
|
c.lastCaddyCtrID = caddyCtr.ID
|
||||||
// Config loaded successfully, now write it to disk.
|
c.lastStartedAt = caddyCtr.State.StartedAt
|
||||||
if err = c.writeCaddyfileIfChanged(caddyfile); err != nil {
|
c.lastGlobal = caddyCtr.ServiceSpec.CaddyConfig()
|
||||||
c.log.Error("Failed to write Caddyfile to disk after successful load.", "err", err)
|
c.lastSavedBody = caddyfileBody(caddyfile)
|
||||||
// Config is already loaded in Caddy, so this is not critical. The next regeneration retries the disk write.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.log.Info("New Caddy configuration loaded into local Caddy instance.", "path", c.caddyfilePath)
|
c.log.Info("New Caddy configuration loaded into local Caddy instance.", "path", c.caddyfilePath)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isBootstrapCaddyfile distinguishes a reduced startup file from a full file that must survive Caddy outage.
|
||||||
|
// It also recognizes the older daemon's offline file. That file omitted every x-caddy block, including globals,
|
||||||
|
// so treating it as a protected full file would preserve the configuration-loss bug:
|
||||||
|
// https://github.com/psviderski/uncloud/issues/412
|
||||||
|
func isBootstrapCaddyfile(caddyfile string) bool {
|
||||||
|
return strings.Contains(caddyfile, bootstrapMarker) || strings.Contains(caddyfile, legacyBootstrapMarker)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fingerprintContainers returns a fingerprint of containers that the Caddyfile generator depends on.
|
// fingerprintContainers returns a fingerprint of containers that the Caddyfile generator depends on.
|
||||||
func fingerprintContainers(containers []store.ContainerRecord) []containerFingerprint {
|
func fingerprintContainers(containers []store.ContainerRecord) []containerFingerprint {
|
||||||
fingerprints := make([]containerFingerprint, len(containers))
|
fingerprints := make([]containerFingerprint, len(containers))
|
||||||
for i, cr := range containers {
|
for i, cr := range containers {
|
||||||
// Ignore ports parsing error as not much we can do about it. The generator just logs them and continues.
|
// Ignore ports parsing error as not much we can do about it. The generator just logs them and skips
|
||||||
|
// the container.
|
||||||
ports, _ := cr.Container.ServicePorts()
|
ports, _ := cr.Container.ServicePorts()
|
||||||
fingerprints[i] = containerFingerprint{
|
fingerprints[i] = containerFingerprint{
|
||||||
ID: cr.Container.ID,
|
ID: cr.Container.ID,
|
||||||
@@ -220,21 +288,71 @@ func fingerprintContainers(containers []store.ContainerRecord) []containerFinger
|
|||||||
return fingerprints
|
return fingerprints
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeCaddyfileIfChanged writes the Caddyfile content to disk with proper permissions only if its body differs
|
// selectLocalCaddyContainer chooses which local Caddy container supplies global Caddy config. A running container
|
||||||
// from the last successfully written content. The first line of the Caddyfile carries a generation timestamp that
|
// takes precedence over a newer stopped replacement, even if the running container is unhealthy. If two revisions
|
||||||
// changes on every regeneration, so it's excluded from the comparison to avoid redundant writes.
|
// run at once, the shared admin socket does not identify its owner, so this choice remains best effort.
|
||||||
|
func selectLocalCaddyContainer(records []store.ContainerRecord, machineID string) *api.ServiceContainer {
|
||||||
|
var selected *api.ServiceContainer
|
||||||
|
for _, cr := range records {
|
||||||
|
ctr := cr.Container
|
||||||
|
if cr.MachineID != machineID || ctr.ServiceName() != CaddyServiceName || ctr.IsHook() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if selected == nil {
|
||||||
|
selected = &ctr
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ctr.State.Running != selected.State.Running {
|
||||||
|
if ctr.State.Running {
|
||||||
|
selected = &ctr
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ctr.CreatedTime().Compare(selected.CreatedTime()) > 0 {
|
||||||
|
selected = &ctr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeCaddyfileIfChanged atomically replaces the saved Caddyfile only if its body differs from the new content.
|
||||||
func (c *Controller) writeCaddyfileIfChanged(caddyfile string) error {
|
func (c *Controller) writeCaddyfileIfChanged(caddyfile string) error {
|
||||||
if caddyfileBody(caddyfile) == caddyfileBody(c.lastCaddyfile) {
|
saved, err := os.ReadFile(c.caddyfilePath)
|
||||||
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return fmt.Errorf("read Caddyfile '%s': %w", c.caddyfilePath, err)
|
||||||
|
}
|
||||||
|
if err == nil && caddyfileBody(caddyfile) == caddyfileBody(string(saved)) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(c.caddyfilePath, []byte(caddyfile), 0o640); err != nil {
|
dir := filepath.Dir(c.caddyfilePath)
|
||||||
return fmt.Errorf("write Caddyfile to file '%s': %w", c.caddyfilePath, err)
|
tmp, err := os.CreateTemp(dir, ".Caddyfile-*")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create temporary Caddyfile: %w", err)
|
||||||
}
|
}
|
||||||
if err := fs.Chown(c.caddyfilePath, "", CaddyGroup); err != nil {
|
defer os.Remove(tmp.Name())
|
||||||
return fmt.Errorf("change owner of Caddyfile '%s': %w", c.caddyfilePath, err)
|
defer tmp.Close()
|
||||||
|
|
||||||
|
if err = tmp.Chmod(0o640); err != nil {
|
||||||
|
return fmt.Errorf("set temporary Caddyfile permissions: %w", err)
|
||||||
|
}
|
||||||
|
if err = fs.Chown(tmp.Name(), "", c.fileGroup); err != nil {
|
||||||
|
return fmt.Errorf("change owner of temporary Caddyfile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = tmp.WriteString(caddyfile); err != nil {
|
||||||
|
return fmt.Errorf("write temporary Caddyfile: %w", err)
|
||||||
|
}
|
||||||
|
if err = tmp.Sync(); err != nil {
|
||||||
|
return fmt.Errorf("sync temporary Caddyfile: %w", err)
|
||||||
|
}
|
||||||
|
if err = tmp.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close temporary Caddyfile: %w", err)
|
||||||
|
}
|
||||||
|
if err = os.Rename(tmp.Name(), c.caddyfilePath); err != nil {
|
||||||
|
return fmt.Errorf("replace Caddyfile '%s': %w", c.caddyfilePath, err)
|
||||||
}
|
}
|
||||||
c.lastCaddyfile = caddyfile
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -247,30 +365,3 @@ func caddyfileBody(caddyfile string) string {
|
|||||||
}
|
}
|
||||||
return caddyfile
|
return caddyfile
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) generateJSONConfig(containers []store.ContainerRecord) error {
|
|
||||||
serviceContainers := make([]api.ServiceContainer, len(containers))
|
|
||||||
for i, cr := range containers {
|
|
||||||
serviceContainers[i] = cr.Container
|
|
||||||
}
|
|
||||||
|
|
||||||
config, err := GenerateJSONConfig(serviceContainers, c.machineID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
configBytes, err := json.MarshalIndent(config, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("marshal Caddy configuration: %w", err)
|
|
||||||
}
|
|
||||||
configPath := filepath.Join(filepath.Dir(c.caddyfilePath), "caddy.json")
|
|
||||||
|
|
||||||
if err = os.WriteFile(configPath, configBytes, 0o640); err != nil {
|
|
||||||
return fmt.Errorf("write Caddy configuration to file '%s': %w", configPath, err)
|
|
||||||
}
|
|
||||||
if err = fs.Chown(configPath, "", CaddyGroup); err != nil {
|
|
||||||
return fmt.Errorf("change owner of Caddy configuration file '%s': %w", configPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +1,488 @@
|
|||||||
package caddyconfig
|
package caddyconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func testController(t *testing.T, socketPath string) *Controller {
|
||||||
|
t.Helper()
|
||||||
|
group, err := user.LookupGroupId(strconv.Itoa(os.Getegid()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
path := filepath.Join(t.TempDir(), "Caddyfile")
|
||||||
|
client := NewCaddyAdminClient(socketPath)
|
||||||
|
return &Controller{
|
||||||
|
machineID: "test-machine-id",
|
||||||
|
caddyfilePath: path,
|
||||||
|
generator: NewCaddyfileGenerator("test-machine-id", "test-machine", client, nil),
|
||||||
|
client: client,
|
||||||
|
log: slog.Default(),
|
||||||
|
fileGroup: group.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testCaddyAdmin struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
adaptRequests []string
|
||||||
|
loadCount int
|
||||||
|
rejectAdaptContaining string
|
||||||
|
rejectLoad bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *testCaddyAdmin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/adapt":
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.mu.Lock()
|
||||||
|
a.adaptRequests = append(a.adaptRequests, string(body))
|
||||||
|
reject := a.rejectAdaptContaining != "" && strings.Contains(string(body), a.rejectAdaptContaining)
|
||||||
|
a.mu.Unlock()
|
||||||
|
if reject {
|
||||||
|
http.Error(w, "invalid Caddyfile", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = io.WriteString(w, `{"result":{}}`)
|
||||||
|
case "/load":
|
||||||
|
a.mu.Lock()
|
||||||
|
a.loadCount++
|
||||||
|
reject := a.rejectLoad
|
||||||
|
a.mu.Unlock()
|
||||||
|
if reject {
|
||||||
|
http.Error(w, "load rejected", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *testCaddyAdmin) snapshot() ([]string, int) {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
return append([]string(nil), a.adaptRequests...), a.loadCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *testCaddyAdmin) setRejectLoad(reject bool) {
|
||||||
|
a.mu.Lock()
|
||||||
|
a.rejectLoad = reject
|
||||||
|
a.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestController_GenerateAndLoadCaddyfile(t *testing.T) {
|
||||||
|
t.Run("keeps the saved file when there is no local Caddy container", func(t *testing.T) {
|
||||||
|
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||||
|
saved := "# previous full Caddyfile\n"
|
||||||
|
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
|
||||||
|
remote := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{ remote }",
|
||||||
|
"other-machine", time.Now())
|
||||||
|
|
||||||
|
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{remote}))
|
||||||
|
got, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, saved, string(got))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("does not publish a bootstrap with an invalid global template", func(t *testing.T) {
|
||||||
|
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||||
|
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{{upstreams",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
|
||||||
|
require.ErrorContains(t, controller.generateAndLoadCaddyfile(context.Background(),
|
||||||
|
[]store.ContainerRecord{caddyCtr}), "render template")
|
||||||
|
_, err := os.Stat(controller.caddyfilePath)
|
||||||
|
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bootstraps unhealthy Caddy with its globals and healthy routes", func(t *testing.T) {
|
||||||
|
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||||
|
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
caddyCtr.Container.State.Health = &container.Health{Status: "unhealthy"}
|
||||||
|
newerStopped := newContainerRecordWithCaddyConfig("caddy", "10.210.0.6", "{\n\tstorage replacement\n}",
|
||||||
|
"test-machine-id", time.Now().Add(time.Minute))
|
||||||
|
newerStopped.Container.State.Running = false
|
||||||
|
app := newContainerRecordWithPorts("web", "10.210.0.3", []string{"app.example.com:8080/http"},
|
||||||
|
"test-machine-id")
|
||||||
|
app.Container.ServiceSpec.Caddy = &api.CaddySpec{Config: "custom.example.com { respond app }"}
|
||||||
|
unhealthy := newContainerRecordWithPorts("unhealthy", "10.210.0.4",
|
||||||
|
[]string{"unhealthy.example.com:8080/http"}, "test-machine-id")
|
||||||
|
unhealthy.Container.State.Health = &container.Health{Status: "unhealthy"}
|
||||||
|
hook := newContainerRecordWithPorts("hook", "10.210.0.5",
|
||||||
|
[]string{"hook.example.com:8080/http"}, "test-machine-id")
|
||||||
|
hook.Container.Config.Labels[api.LabelHook] = "pre-deploy"
|
||||||
|
|
||||||
|
err := controller.generateAndLoadCaddyfile(context.Background(),
|
||||||
|
[]store.ContainerRecord{newerStopped, caddyCtr, app, unhealthy, hook})
|
||||||
|
require.ErrorContains(t, err, "admin socket unavailable")
|
||||||
|
saved, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(saved), bootstrapMarker+caddyCtr.Container.ID)
|
||||||
|
assert.Contains(t, string(saved), "storage uncloud")
|
||||||
|
assert.NotContains(t, string(saved), "storage replacement")
|
||||||
|
assert.Contains(t, string(saved), "app.example.com")
|
||||||
|
assert.NotContains(t, string(saved), "custom.example.com")
|
||||||
|
assert.NotContains(t, string(saved), "unhealthy.example.com")
|
||||||
|
assert.NotContains(t, string(saved), "hook.example.com")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("replaces a legacy offline file with a bootstrap containing globals", func(t *testing.T) {
|
||||||
|
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||||
|
legacy := "# generated by older daemon\nhttp:// { respond ok }\n" + legacyBootstrapMarker + "\n"
|
||||||
|
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(legacy), 0o640))
|
||||||
|
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
|
||||||
|
err := controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr})
|
||||||
|
require.ErrorContains(t, err, "admin socket unavailable")
|
||||||
|
saved, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(saved), "storage uncloud")
|
||||||
|
assert.Contains(t, string(saved), bootstrapMarker+caddyCtr.Container.ID)
|
||||||
|
assert.NotContains(t, string(saved), legacyBootstrapMarker)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("saves a bootstrap for a stopped Caddy container with no file", func(t *testing.T) {
|
||||||
|
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||||
|
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
caddyCtr.Container.State.Running = false
|
||||||
|
|
||||||
|
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr}))
|
||||||
|
saved, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(saved), bootstrapMarker+caddyCtr.Container.ID)
|
||||||
|
assert.Contains(t, string(saved), "storage uncloud")
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
running bool
|
||||||
|
}{
|
||||||
|
{name: "preserves a full file when the admin socket is unavailable", running: true},
|
||||||
|
{name: "preserves a stopped container's full file", running: false},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||||
|
saved := "{\n\tstorage uncloud\n}\n\nold.example.com { respond old }\n"
|
||||||
|
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
|
||||||
|
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage new\n}",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
caddyCtr.Container.State.Running = tc.running
|
||||||
|
app := newContainerRecordWithPorts("web", "10.210.0.3", []string{"new.example.com:8080/http"},
|
||||||
|
"test-machine-id")
|
||||||
|
|
||||||
|
err := controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr, app})
|
||||||
|
if tc.running {
|
||||||
|
require.ErrorContains(t, err, "admin socket unavailable")
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
got, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, saved, string(got))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("loads a full file and reloads after Caddy restarts", func(t *testing.T) {
|
||||||
|
admin := &testCaddyAdmin{}
|
||||||
|
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
|
||||||
|
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
caddyCtr.Container.State.Health = &container.Health{Status: "unhealthy"}
|
||||||
|
app := newContainerRecordWithCaddyConfig("web", "10.210.0.3", "app.example.com { respond app }",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
records := []store.ContainerRecord{caddyCtr, app}
|
||||||
|
|
||||||
|
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||||
|
saved, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(saved), "storage uncloud")
|
||||||
|
assert.Contains(t, string(saved), "app.example.com")
|
||||||
|
assert.NotContains(t, string(saved), bootstrapMarker)
|
||||||
|
adapted, loads := admin.snapshot()
|
||||||
|
require.NotEmpty(t, adapted)
|
||||||
|
assert.Contains(t, adapted[0], "storage uncloud")
|
||||||
|
assert.Equal(t, 1, loads)
|
||||||
|
|
||||||
|
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||||
|
_, loads = admin.snapshot()
|
||||||
|
assert.Equal(t, 1, loads, "unchanged inputs should not reload Caddy")
|
||||||
|
|
||||||
|
records[0].Container.State.StartedAt = time.Now().UTC().Format(time.RFC3339Nano)
|
||||||
|
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||||
|
_, loads = admin.snapshot()
|
||||||
|
assert.Equal(t, 2, loads, "a restarted Caddy process needs the full configuration")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("reloads when an application route changes", func(t *testing.T) {
|
||||||
|
admin := &testCaddyAdmin{}
|
||||||
|
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
|
||||||
|
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
app := newContainerRecordWithPorts("web", "10.210.0.3", []string{"app.example.com:8080/http"},
|
||||||
|
"test-machine-id")
|
||||||
|
records := []store.ContainerRecord{caddyCtr, app}
|
||||||
|
|
||||||
|
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||||
|
_, loads := admin.snapshot()
|
||||||
|
assert.Equal(t, 1, loads)
|
||||||
|
|
||||||
|
records[1] = newContainerRecordWithPorts("web", "10.210.0.4",
|
||||||
|
[]string{"app.example.com:8080/http"}, "test-machine-id")
|
||||||
|
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||||
|
saved, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(saved), "10.210.0.4:8080")
|
||||||
|
assert.NotContains(t, string(saved), "10.210.0.3:8080")
|
||||||
|
_, loads = admin.snapshot()
|
||||||
|
assert.Equal(t, 2, loads)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rejects invalid globals without loading or replacing the saved file", func(t *testing.T) {
|
||||||
|
admin := &testCaddyAdmin{rejectAdaptContaining: "storage rejected"}
|
||||||
|
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
|
||||||
|
saved := "{\n\tstorage uncloud\n}\n\nold.example.com { respond old }\n"
|
||||||
|
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
|
||||||
|
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage rejected\n}",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
|
||||||
|
err := controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr})
|
||||||
|
require.ErrorContains(t, err, "validate user-defined global Caddy config")
|
||||||
|
got, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, saved, string(got))
|
||||||
|
_, loads := admin.snapshot()
|
||||||
|
assert.Zero(t, loads)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("skips invalid application custom config", func(t *testing.T) {
|
||||||
|
admin := &testCaddyAdmin{rejectAdaptContaining: "invalid.example.com"}
|
||||||
|
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
|
||||||
|
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
invalid := newContainerRecordWithCaddyConfig("invalid", "10.210.0.3",
|
||||||
|
"invalid.example.com { respond bad }", "test-machine-id", time.Now())
|
||||||
|
valid := newContainerRecordWithCaddyConfig("valid", "10.210.0.4",
|
||||||
|
"valid.example.com { respond good }", "test-machine-id", time.Now())
|
||||||
|
|
||||||
|
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(),
|
||||||
|
[]store.ContainerRecord{caddyCtr, invalid, valid}))
|
||||||
|
saved, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(saved), "storage uncloud")
|
||||||
|
assert.Contains(t, string(saved), "valid.example.com")
|
||||||
|
assert.NotContains(t, string(saved), "invalid.example.com")
|
||||||
|
assert.Contains(t, string(saved), "Skipped invalid user-defined configs")
|
||||||
|
_, loads := admin.snapshot()
|
||||||
|
assert.Equal(t, 1, loads)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("keeps the saved file when application validation cannot complete", func(t *testing.T) {
|
||||||
|
var loads atomic.Int64
|
||||||
|
socket := testAdminServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/adapt":
|
||||||
|
http.Error(w, "adapter unavailable", http.StatusInternalServerError)
|
||||||
|
case "/load":
|
||||||
|
loads.Add(1)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
controller := testController(t, socket)
|
||||||
|
saved := "old.example.com { respond old }\n"
|
||||||
|
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
|
||||||
|
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
app := newContainerRecordWithCaddyConfig("web", "10.210.0.3", "new.example.com { respond new }",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
|
||||||
|
err := controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr, app})
|
||||||
|
require.ErrorContains(t, err, "validate Caddy config for service")
|
||||||
|
got, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, saved, string(got))
|
||||||
|
assert.Zero(t, loads.Load())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("retries a rejected load without changing inputs", func(t *testing.T) {
|
||||||
|
admin := &testCaddyAdmin{rejectLoad: true}
|
||||||
|
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
|
||||||
|
saved := "{\n\tstorage old\n}\n\nold.example.com { respond old }\n"
|
||||||
|
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
|
||||||
|
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
records := []store.ContainerRecord{caddyCtr}
|
||||||
|
|
||||||
|
require.ErrorContains(t, controller.generateAndLoadCaddyfile(context.Background(), records), "load Caddyfile")
|
||||||
|
got, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, saved, string(got))
|
||||||
|
admin.setRejectLoad(false)
|
||||||
|
|
||||||
|
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||||
|
got, err = os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(got), "storage uncloud")
|
||||||
|
_, loads := admin.snapshot()
|
||||||
|
assert.Equal(t, 2, loads)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("retries saving after a successful load", func(t *testing.T) {
|
||||||
|
admin := &testCaddyAdmin{}
|
||||||
|
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
|
||||||
|
group := controller.fileGroup
|
||||||
|
controller.fileGroup = "uncloud-test-group-that-does-not-exist"
|
||||||
|
saved := "{\n\tstorage old\n}\n\nold.example.com { respond old }\n"
|
||||||
|
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
|
||||||
|
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
records := []store.ContainerRecord{caddyCtr}
|
||||||
|
|
||||||
|
require.ErrorContains(t, controller.generateAndLoadCaddyfile(context.Background(), records), "save loaded Caddyfile")
|
||||||
|
got, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, saved, string(got))
|
||||||
|
controller.fileGroup = group
|
||||||
|
|
||||||
|
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||||
|
got, err = os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(got), "storage uncloud")
|
||||||
|
_, loads := admin.snapshot()
|
||||||
|
assert.Equal(t, 2, loads)
|
||||||
|
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||||
|
_, loads = admin.snapshot()
|
||||||
|
assert.Equal(t, 2, loads, "a loaded and saved revision should not reload")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("loads a saved bootstrap when the admin socket becomes available", func(t *testing.T) {
|
||||||
|
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||||
|
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
records := []store.ContainerRecord{caddyCtr}
|
||||||
|
require.Error(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||||
|
bootstrap, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(bootstrap), bootstrapMarker)
|
||||||
|
|
||||||
|
admin := &testCaddyAdmin{}
|
||||||
|
socket := testAdminServer(t, admin.ServeHTTP)
|
||||||
|
controller.client = NewCaddyAdminClient(socket)
|
||||||
|
controller.generator = NewCaddyfileGenerator("test-machine-id", "test-machine", controller.client, nil)
|
||||||
|
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||||
|
full, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotContains(t, string(full), bootstrapMarker)
|
||||||
|
_, loads := admin.snapshot()
|
||||||
|
assert.Equal(t, 1, loads)
|
||||||
|
|
||||||
|
require.NoError(t, os.WriteFile(controller.caddyfilePath, bootstrap, 0o640))
|
||||||
|
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||||
|
_, loads = admin.snapshot()
|
||||||
|
assert.Equal(t, 2, loads, "a saved bootstrap is not an applied full configuration")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestController_WriteCaddyfileIfChanged(t *testing.T) {
|
||||||
|
t.Run("publishes a complete file with the configured group", func(t *testing.T) {
|
||||||
|
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||||
|
caddyfile := "# Generated now\n{\n\tstorage uncloud\n}\n"
|
||||||
|
|
||||||
|
require.NoError(t, controller.writeCaddyfileIfChanged(caddyfile))
|
||||||
|
got, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, caddyfile, string(got))
|
||||||
|
info, err := os.Stat(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, os.FileMode(0o640), info.Mode().Perm())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("does not replace a file when only the timestamp changes", func(t *testing.T) {
|
||||||
|
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||||
|
saved := "# Generated yesterday\nsite.example.com { respond ok }\n"
|
||||||
|
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
|
||||||
|
before, err := os.Stat(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, controller.writeCaddyfileIfChanged(
|
||||||
|
"# Generated today\nsite.example.com { respond ok }\n"))
|
||||||
|
got, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, saved, string(got))
|
||||||
|
after, err := os.Stat(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, os.SameFile(before, after), "an unchanged body should not replace the file")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("preserves the previous file when publication fails", func(t *testing.T) {
|
||||||
|
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||||
|
controller.fileGroup = "uncloud-test-group-that-does-not-exist"
|
||||||
|
saved := "# previous\nsite.example.com { respond old }\n"
|
||||||
|
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
|
||||||
|
|
||||||
|
err := controller.writeCaddyfileIfChanged("# next\nsite.example.com { respond new }\n")
|
||||||
|
require.ErrorContains(t, err, "change owner of temporary Caddyfile")
|
||||||
|
got, err := os.ReadFile(controller.caddyfilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, saved, string(got))
|
||||||
|
tmpFiles, err := filepath.Glob(filepath.Join(filepath.Dir(controller.caddyfilePath), ".Caddyfile-*"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, tmpFiles)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectLocalCaddyContainer(t *testing.T) {
|
||||||
|
t.Run("prefers a running unhealthy container over a newer stopped one", func(t *testing.T) {
|
||||||
|
older := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{ older }",
|
||||||
|
"test-machine-id", time.Now().Add(-time.Hour))
|
||||||
|
older.Container.State.Health = &container.Health{Status: "unhealthy"}
|
||||||
|
newer := newContainerRecordWithCaddyConfig("caddy", "10.210.0.3", "{ newer }",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
newer.Container.State.Running = false
|
||||||
|
|
||||||
|
selected := selectLocalCaddyContainer([]store.ContainerRecord{newer, older}, "test-machine-id")
|
||||||
|
require.NotNil(t, selected)
|
||||||
|
assert.Equal(t, older.Container.ID, selected.ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ignores remote and hook containers", func(t *testing.T) {
|
||||||
|
local := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{ local }",
|
||||||
|
"test-machine-id", time.Now().Add(-time.Hour))
|
||||||
|
remote := newContainerRecordWithCaddyConfig("caddy", "10.210.0.3", "{ remote }",
|
||||||
|
"other-machine", time.Now())
|
||||||
|
hook := newContainerRecordWithCaddyConfig("caddy", "10.210.0.4", "{ hook }",
|
||||||
|
"test-machine-id", time.Now())
|
||||||
|
hook.Container.Config.Labels[api.LabelHook] = "pre-deploy"
|
||||||
|
|
||||||
|
selected := selectLocalCaddyContainer([]store.ContainerRecord{remote, hook, local}, "test-machine-id")
|
||||||
|
require.NotNil(t, selected)
|
||||||
|
assert.Equal(t, local.Container.ID, selected.ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// TestContainerFingerprint_EqualCoversAllFields is a guard: when a field is added to containerFingerprint,
|
// TestContainerFingerprint_EqualCoversAllFields is a guard: when a field is added to containerFingerprint,
|
||||||
// Equal must also compare it. A mutation of any single field should flip equality to false. If this test fails
|
// Equal must also compare it. A mutation of any single field should flip equality to false. If this test fails
|
||||||
// after adding a field, update Equal to include it.
|
// after adding a field, update Equal to include it.
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
package caddyconfig
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"maps"
|
|
||||||
"net/http"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GenerateJSONConfig(containers []api.ServiceContainer, verifyResponse string) (*caddy.Config, error) {
|
|
||||||
httpHostUpstreams, httpsHostUpstreams := httpUpstreamsFromPorts(containers)
|
|
||||||
|
|
||||||
var warnings []caddyconfig.Warning
|
|
||||||
servers := make(map[string]*caddyhttp.Server)
|
|
||||||
servers["http"] = &caddyhttp.Server{
|
|
||||||
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPPort)},
|
|
||||||
// All http requests to this server are logged to the default logger.
|
|
||||||
Logs: &caddyhttp.ServerLogConfig{},
|
|
||||||
Routes: append(
|
|
||||||
hostUpstreamsToRoutes(httpHostUpstreams, &warnings),
|
|
||||||
// Add a route to respond with a static verification response at the /.uncloud-verify path.
|
|
||||||
verificationRoute(verifyResponse, &warnings),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
servers["https"] = &caddyhttp.Server{
|
|
||||||
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPSPort)},
|
|
||||||
// All https requests to this server are logged to the default logger.
|
|
||||||
Logs: &caddyhttp.ServerLogConfig{},
|
|
||||||
Routes: hostUpstreamsToRoutes(httpsHostUpstreams, &warnings),
|
|
||||||
}
|
|
||||||
|
|
||||||
httpApp := caddyhttp.App{
|
|
||||||
Servers: servers,
|
|
||||||
}
|
|
||||||
config := &caddy.Config{
|
|
||||||
AppsRaw: caddy.ModuleMap{
|
|
||||||
"http": caddyconfig.JSON(httpApp, &warnings),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if len(warnings) > 0 {
|
|
||||||
// warnings only contains errors from JSON marshaling, which are highly unlikely with correct code.
|
|
||||||
for _, w := range warnings {
|
|
||||||
err = errors.Join(err, errors.New(w.Message))
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("marshal Caddy configuration: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// hostUpstreamsToRoutes converts a map of hostnames to upstreams to a list of Caddy routes.
|
|
||||||
func hostUpstreamsToRoutes(hostUpstreams map[string][]string, warnings *[]caddyconfig.Warning) []caddyhttp.Route {
|
|
||||||
// Sort hostnames for deterministic output.
|
|
||||||
hostnames := slices.Collect(maps.Keys(hostUpstreams))
|
|
||||||
slices.Sort(hostnames)
|
|
||||||
|
|
||||||
routes := make([]caddyhttp.Route, 0, len(hostUpstreams))
|
|
||||||
for _, hostname := range hostnames {
|
|
||||||
upstreams := hostUpstreams[hostname]
|
|
||||||
upstreamPool := make([]*reverseproxy.Upstream, len(upstreams))
|
|
||||||
for i, upstream := range upstreams {
|
|
||||||
upstreamPool[i] = &reverseproxy.Upstream{
|
|
||||||
Dial: upstream,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handler := &reverseproxy.Handler{
|
|
||||||
HealthChecks: &reverseproxy.HealthChecks{
|
|
||||||
// Enable passive health checks to automatically detect unhealthy upstreams.
|
|
||||||
Passive: &reverseproxy.PassiveHealthChecks{
|
|
||||||
FailDuration: caddy.Duration(30 * time.Second),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
LoadBalancing: &reverseproxy.LoadBalancing{
|
|
||||||
// Retry failed requests to skip over temporarily unavailable upstreams.
|
|
||||||
Retries: 3,
|
|
||||||
},
|
|
||||||
Upstreams: upstreamPool,
|
|
||||||
}
|
|
||||||
|
|
||||||
routes = append(routes, caddyhttp.Route{
|
|
||||||
MatcherSetsRaw: caddyhttp.RawMatcherSets{
|
|
||||||
{
|
|
||||||
"host": caddyconfig.JSON(caddyhttp.MatchHost{hostname}, warnings),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
HandlersRaw: []json.RawMessage{
|
|
||||||
caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", warnings),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return routes
|
|
||||||
}
|
|
||||||
|
|
||||||
// verificationRoute returns a Caddy route that responds with the given static response at the /.uncloud-verify path.
|
|
||||||
func verificationRoute(response string, warnings *[]caddyconfig.Warning) caddyhttp.Route {
|
|
||||||
// Return the following route:
|
|
||||||
// {
|
|
||||||
// "match": [
|
|
||||||
// {
|
|
||||||
// "path": [
|
|
||||||
// "/.uncloud-verify"
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
// ],
|
|
||||||
// "handle": [
|
|
||||||
// {
|
|
||||||
// "handler": "static_response",
|
|
||||||
// "body": "<response>",
|
|
||||||
// "status_code": 200
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
|
|
||||||
staticResponse := caddyhttp.StaticResponse{
|
|
||||||
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusOK)),
|
|
||||||
Body: response,
|
|
||||||
}
|
|
||||||
|
|
||||||
return caddyhttp.Route{
|
|
||||||
MatcherSetsRaw: caddyhttp.RawMatcherSets{
|
|
||||||
{
|
|
||||||
"path": caddyconfig.JSON(caddyhttp.MatchPath{VerifyPath}, warnings),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
HandlersRaw: []json.RawMessage{
|
|
||||||
caddyconfig.JSONModuleObject(staticResponse, "handler", "static_response", warnings),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,378 +0,0 @@
|
|||||||
package caddyconfig
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/docker/docker/api/types/network"
|
|
||||||
"github.com/psviderski/uncloud/internal/machine/docker"
|
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGenerateJSONConfig(t *testing.T) {
|
|
||||||
configWithoutServices := `{
|
|
||||||
"servers": {
|
|
||||||
"http": {
|
|
||||||
"listen": [":80"],
|
|
||||||
"routes": [{
|
|
||||||
"match": [{"path": ["/.uncloud-verify"]}],
|
|
||||||
"handle": [{
|
|
||||||
"body": "verification-response-body",
|
|
||||||
"handler": "static_response",
|
|
||||||
"status_code": 200
|
|
||||||
}]
|
|
||||||
}],
|
|
||||||
"logs": {}
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"listen": [":443"],
|
|
||||||
"logs": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
containers []api.ServiceContainer
|
|
||||||
want string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty containers",
|
|
||||||
containers: []api.ServiceContainer{},
|
|
||||||
want: configWithoutServices,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "HTTP container",
|
|
||||||
containers: []api.ServiceContainer{
|
|
||||||
newContainer("10.210.0.2", "app.example.com:8080/http"),
|
|
||||||
},
|
|
||||||
want: `{
|
|
||||||
"servers": {
|
|
||||||
"http": {
|
|
||||||
"listen": [":80"],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [{"host": ["app.example.com"]}],
|
|
||||||
"handle": [{
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"health_checks": {
|
|
||||||
"passive": {
|
|
||||||
"fail_duration": 30000000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"load_balancing": {
|
|
||||||
"retries": 3
|
|
||||||
},
|
|
||||||
"upstreams": [{"dial": "10.210.0.2:8080"}]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": [{"path": ["/.uncloud-verify"]}],
|
|
||||||
"handle": [{
|
|
||||||
"body": "verification-response-body",
|
|
||||||
"handler": "static_response",
|
|
||||||
"status_code": 200
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"logs": {}
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"listen": [":443"],
|
|
||||||
"logs": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "load balancing multiple containers",
|
|
||||||
containers: []api.ServiceContainer{
|
|
||||||
newContainer("10.210.0.2", "app.example.com:8080/http"),
|
|
||||||
newContainer("10.210.0.3", "app.example.com:8080/http"),
|
|
||||||
},
|
|
||||||
want: `{
|
|
||||||
"servers": {
|
|
||||||
"http": {
|
|
||||||
"listen": [":80"],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [{"host": ["app.example.com"]}],
|
|
||||||
"handle": [{
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"health_checks": {
|
|
||||||
"passive": {
|
|
||||||
"fail_duration": 30000000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"load_balancing": {
|
|
||||||
"retries": 3
|
|
||||||
},
|
|
||||||
"upstreams": [
|
|
||||||
{"dial": "10.210.0.2:8080"},
|
|
||||||
{"dial": "10.210.0.3:8080"}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": [{"path": ["/.uncloud-verify"]}],
|
|
||||||
"handle": [{
|
|
||||||
"body": "verification-response-body",
|
|
||||||
"handler": "static_response",
|
|
||||||
"status_code": 200
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"logs": {}
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"listen": [":443"],
|
|
||||||
"logs": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "HTTPS container",
|
|
||||||
containers: []api.ServiceContainer{
|
|
||||||
newContainer("10.210.0.2", "secure.example.com:8000/https"),
|
|
||||||
},
|
|
||||||
want: `{
|
|
||||||
"servers": {
|
|
||||||
"http": {
|
|
||||||
"listen": [":80"],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [{"path": ["/.uncloud-verify"]}],
|
|
||||||
"handle": [{
|
|
||||||
"body": "verification-response-body",
|
|
||||||
"handler": "static_response",
|
|
||||||
"status_code": 200
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"logs": {}
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"listen": [":443"],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [{"host": ["secure.example.com"]}],
|
|
||||||
"handle": [{
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"health_checks": {
|
|
||||||
"passive": {
|
|
||||||
"fail_duration": 30000000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"load_balancing": {
|
|
||||||
"retries": 3
|
|
||||||
},
|
|
||||||
"upstreams": [{"dial": "10.210.0.2:8000"}]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"logs": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mixed HTTP and HTTPS",
|
|
||||||
containers: []api.ServiceContainer{
|
|
||||||
newContainer("10.210.0.2",
|
|
||||||
"app.example.com:8080/http",
|
|
||||||
"web.example.com:8000/http"),
|
|
||||||
newContainer("10.210.0.3",
|
|
||||||
"app.example.com:8080/http",
|
|
||||||
"secure.example.com:8888/https"),
|
|
||||||
newContainer("10.210.0.4",
|
|
||||||
"web.example.com:8000/http",
|
|
||||||
"secure.example.com:8888/https"),
|
|
||||||
newContainer("10.210.0.5",
|
|
||||||
"app.example.com:8080/http",
|
|
||||||
"web.example.com:8000/http",
|
|
||||||
"secure.example.com:8888/https"),
|
|
||||||
},
|
|
||||||
want: `{
|
|
||||||
"servers": {
|
|
||||||
"http": {
|
|
||||||
"listen": [":80"],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [{"host": ["app.example.com"]}],
|
|
||||||
"handle": [{
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"health_checks": {
|
|
||||||
"passive": {
|
|
||||||
"fail_duration": 30000000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"load_balancing": {
|
|
||||||
"retries": 3
|
|
||||||
},
|
|
||||||
"upstreams": [
|
|
||||||
{"dial": "10.210.0.2:8080"},
|
|
||||||
{"dial": "10.210.0.3:8080"},
|
|
||||||
{"dial": "10.210.0.5:8080"}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": [{"host": ["web.example.com"]}],
|
|
||||||
"handle": [{
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"health_checks": {
|
|
||||||
"passive": {
|
|
||||||
"fail_duration": 30000000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"load_balancing": {
|
|
||||||
"retries": 3
|
|
||||||
},
|
|
||||||
"upstreams": [
|
|
||||||
{"dial": "10.210.0.2:8000"},
|
|
||||||
{"dial": "10.210.0.4:8000"},
|
|
||||||
{"dial": "10.210.0.5:8000"}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": [{"path": ["/.uncloud-verify"]}],
|
|
||||||
"handle": [{
|
|
||||||
"body": "verification-response-body",
|
|
||||||
"handler": "static_response",
|
|
||||||
"status_code": 200
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"logs": {}
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"listen": [":443"],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [{"host": ["secure.example.com"]}],
|
|
||||||
"handle": [{
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"health_checks": {
|
|
||||||
"passive": {
|
|
||||||
"fail_duration": 30000000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"load_balancing": {
|
|
||||||
"retries": 3
|
|
||||||
},
|
|
||||||
"upstreams": [
|
|
||||||
{"dial": "10.210.0.3:8888"},
|
|
||||||
{"dial": "10.210.0.4:8888"},
|
|
||||||
{"dial": "10.210.0.5:8888"}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"logs": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "container without uncloud network ignored",
|
|
||||||
containers: []api.ServiceContainer{
|
|
||||||
newContainerWithoutNetwork("ignored.example.com:8080/http"),
|
|
||||||
},
|
|
||||||
want: configWithoutServices,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "container with invalid port ignored",
|
|
||||||
containers: []api.ServiceContainer{
|
|
||||||
newContainer("10.210.0.2", "invalid-port"),
|
|
||||||
},
|
|
||||||
want: configWithoutServices,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "containers with unsupported protocols and host mode ignored",
|
|
||||||
containers: []api.ServiceContainer{
|
|
||||||
newContainer("10.210.0.2", "5000/tcp"),
|
|
||||||
newContainer("10.210.0.3", "5000/udp"),
|
|
||||||
newContainer("10.210.0.4", "80:8080/tcp@host"),
|
|
||||||
},
|
|
||||||
want: configWithoutServices,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
config, err := GenerateJSONConfig(tt.containers, "verification-response-body")
|
|
||||||
|
|
||||||
if tt.wantErr {
|
|
||||||
assert.Error(t, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Len(t, config.AppsRaw, 1, "Expected one http app")
|
|
||||||
require.Contains(t, config.AppsRaw, "http", "Expected http app")
|
|
||||||
|
|
||||||
assert.JSONEq(t, tt.want, string(config.AppsRaw["http"]), "Generated Caddy app config doesn't match")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newContainer(ip string, ports ...string) api.ServiceContainer {
|
|
||||||
portsLabel := strings.Join(ports, ",")
|
|
||||||
return api.ServiceContainer{Container: api.Container{InspectResponse: container.InspectResponse{
|
|
||||||
ContainerJSONBase: &container.ContainerJSONBase{
|
|
||||||
State: &container.State{
|
|
||||||
Running: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
NetworkSettings: &container.NetworkSettings{
|
|
||||||
Networks: map[string]*network.EndpointSettings{
|
|
||||||
docker.NetworkName: {
|
|
||||||
IPAddress: ip,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Config: &container.Config{
|
|
||||||
Labels: map[string]string{
|
|
||||||
api.LabelServicePorts: portsLabel,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
|
|
||||||
portsLabel := strings.Join(ports, ",")
|
|
||||||
return api.ServiceContainer{Container: api.Container{InspectResponse: container.InspectResponse{
|
|
||||||
ContainerJSONBase: &container.ContainerJSONBase{
|
|
||||||
State: &container.State{
|
|
||||||
Running: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
NetworkSettings: &container.NetworkSettings{
|
|
||||||
Networks: map[string]*network.EndpointSettings{
|
|
||||||
"other-network": {
|
|
||||||
IPAddress: "172.17.0.2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Config: &container.Config{
|
|
||||||
Labels: map[string]string{
|
|
||||||
api.LabelServicePorts: portsLabel,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}}
|
|
||||||
}
|
|
||||||
@@ -16,10 +16,19 @@ func NewMockCaddyfileValidator(t interface {
|
|||||||
mock.TestingT
|
mock.TestingT
|
||||||
Cleanup(func())
|
Cleanup(func())
|
||||||
}) *MockCaddyfileValidator {
|
}) *MockCaddyfileValidator {
|
||||||
|
if helper, ok := t.(interface{ Helper() }); ok {
|
||||||
|
helper.Helper()
|
||||||
|
}
|
||||||
|
|
||||||
mock := &MockCaddyfileValidator{}
|
mock := &MockCaddyfileValidator{}
|
||||||
mock.Mock.Test(t)
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
t.Cleanup(func() {
|
||||||
|
if helper, ok := t.(interface{ Helper() }); ok {
|
||||||
|
helper.Helper()
|
||||||
|
}
|
||||||
|
mock.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
|
||||||
return mock
|
return mock
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package caddyconfig
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
@@ -22,18 +23,26 @@ func NewServer(service *Service) *Server {
|
|||||||
return &Server{service: service}
|
return &Server{service: service}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfig retrieves the current Caddy configuration from the machine.
|
// GetConfig retrieves the saved Caddy configuration and the latest reconciliation error from the machine.
|
||||||
func (s *Server) GetConfig(ctx context.Context, _ *emptypb.Empty) (*pb.GetCaddyConfigResponse, error) {
|
func (s *Server) GetConfig(ctx context.Context, _ *emptypb.Empty) (*pb.GetCaddyConfigResponse, error) {
|
||||||
caddyfile, modifiedAt, err := s.service.Caddyfile()
|
caddyfile, modifiedAt, err := s.service.Caddyfile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if lastErr := s.service.LastReconciliationError(); lastErr != nil {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "%v; last Caddy config load failed: %v", err, lastErr)
|
||||||
|
}
|
||||||
return nil, status.Error(codes.NotFound, err.Error())
|
return nil, status.Error(codes.NotFound, err.Error())
|
||||||
}
|
}
|
||||||
return nil, status.Error(codes.Internal, err.Error())
|
return nil, status.Error(codes.Internal, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recErr := ""
|
||||||
|
if lastErr := s.service.LastReconciliationError(); lastErr != nil {
|
||||||
|
recErr = lastErr.Error()
|
||||||
|
}
|
||||||
return &pb.GetCaddyConfigResponse{
|
return &pb.GetCaddyConfigResponse{
|
||||||
Caddyfile: caddyfile,
|
Caddyfile: caddyfile,
|
||||||
ModifiedAt: timestamppb.New(modifiedAt),
|
ModifiedAt: timestamppb.New(modifiedAt),
|
||||||
|
LastReconciliationError: recErr,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package caddyconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServer_GetConfig(t *testing.T) {
|
||||||
|
t.Run("returns saved Caddyfile and reconciliation error", func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
caddyfile := "example.com { respond ok }\n"
|
||||||
|
path := filepath.Join(dir, "Caddyfile")
|
||||||
|
require.NoError(t, os.WriteFile(path, []byte(caddyfile), 0o640))
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
service := NewService(dir)
|
||||||
|
service.setReconciliationResult(errors.New("module not registered: caddy.storage.uncloud"))
|
||||||
|
config, err := NewServer(service).GetConfig(context.Background(), &emptypb.Empty{})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, caddyfile, config.GetCaddyfile())
|
||||||
|
assert.True(t, info.ModTime().Equal(config.GetModifiedAt().AsTime()))
|
||||||
|
assert.Equal(t, "module not registered: caddy.storage.uncloud", config.GetLastReconciliationError())
|
||||||
|
|
||||||
|
service.setReconciliationResult(nil)
|
||||||
|
config, err = NewServer(service).GetConfig(context.Background(), &emptypb.Empty{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, config.GetLastReconciliationError())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns NotFound with reconciliation error when Caddyfile is absent", func(t *testing.T) {
|
||||||
|
service := NewService(t.TempDir())
|
||||||
|
service.setReconciliationResult(errors.New("module not registered: caddy.storage.uncloud"))
|
||||||
|
|
||||||
|
config, err := NewServer(service).GetConfig(context.Background(), &emptypb.Empty{})
|
||||||
|
|
||||||
|
assert.Nil(t, config)
|
||||||
|
assert.Equal(t, codes.NotFound, status.Code(err))
|
||||||
|
assert.ErrorContains(t, err, "Caddyfile")
|
||||||
|
assert.ErrorContains(t, err, "last Caddy config load failed: module not registered: caddy.storage.uncloud")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns NotFound without reconciliation error when Caddyfile is absent", func(t *testing.T) {
|
||||||
|
service := NewService(t.TempDir())
|
||||||
|
|
||||||
|
config, err := NewServer(service).GetConfig(context.Background(), &emptypb.Empty{})
|
||||||
|
|
||||||
|
assert.Nil(t, config)
|
||||||
|
assert.Equal(t, codes.NotFound, status.Code(err))
|
||||||
|
assert.NotContains(t, err.Error(), "last Caddy config load failed")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,12 +4,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service provides methods to interact with the Caddy configuration on the machine.
|
// Service provides methods to interact with the Caddy configuration on the machine.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
configDir string
|
configDir string
|
||||||
|
mu sync.RWMutex
|
||||||
|
lastReconciliationError error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new Service instance with the specified Caddy configuration directory.
|
// NewService creates a new Service instance with the specified Caddy configuration directory.
|
||||||
@@ -17,7 +20,8 @@ func NewService(configDir string) *Service {
|
|||||||
return &Service{configDir: configDir}
|
return &Service{configDir: configDir}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caddyfile retrieves the current Caddy configuration (Caddyfile) from the machine's config directory.
|
// Caddyfile retrieves the saved Caddyfile from the machine's config directory. The saved file may differ from the
|
||||||
|
// running configuration if Caddy accepted a load but the subsequent write failed.
|
||||||
func (s *Service) Caddyfile() (string, time.Time, error) {
|
func (s *Service) Caddyfile() (string, time.Time, error) {
|
||||||
path := filepath.Join(s.configDir, "Caddyfile")
|
path := filepath.Join(s.configDir, "Caddyfile")
|
||||||
content, err := os.ReadFile(path)
|
content, err := os.ReadFile(path)
|
||||||
@@ -33,3 +37,17 @@ func (s *Service) Caddyfile() (string, time.Time, error) {
|
|||||||
|
|
||||||
return string(content), fileInfo.ModTime(), nil
|
return string(content), fileInfo.ModTime(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LastReconciliationError reports the most recent unsuccessful controller attempt, if any. A successful attempt
|
||||||
|
// clears it. An empty value does not prove that Caddy has loaded the saved Caddyfile.
|
||||||
|
func (s *Service) LastReconciliationError() error {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
return s.lastReconciliationError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) setReconciliationResult(err error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.lastReconciliationError = err
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package caddyconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_LastReconciliationError(t *testing.T) {
|
||||||
|
service := NewService(t.TempDir())
|
||||||
|
assert.NoError(t, service.LastReconciliationError())
|
||||||
|
|
||||||
|
service.setReconciliationResult(errors.New("global Caddy config rejected"))
|
||||||
|
assert.EqualError(t, service.LastReconciliationError(), "global Caddy config rejected")
|
||||||
|
|
||||||
|
service.setReconciliationResult(nil)
|
||||||
|
assert.NoError(t, service.LastReconciliationError())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_LastReconciliationErrorConcurrent(t *testing.T) {
|
||||||
|
service := NewService(t.TempDir())
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for range 10 {
|
||||||
|
wg.Add(2)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for range 100 {
|
||||||
|
service.setReconciliationResult(errors.New("retry"))
|
||||||
|
service.setReconciliationResult(nil)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for range 100 {
|
||||||
|
_ = service.LastReconciliationError()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package caddystorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/psviderski/uncloud/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListCertificates lists issued certificates from this machine's Caddy storage replica.
|
||||||
|
// Unreadable entries and invalid resource metadata are skipped. Chains and issuer data are returned as stored.
|
||||||
|
func (s *Server) ListCertificates(ctx context.Context, _ *emptypb.Empty) (*pb.ListCertificatesResponse, error) {
|
||||||
|
records, err := s.store.List(ctx, "certificates/", store.KeyspaceListOptions{KeysOnly: true})
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil, status.FromContextError(ctx.Err()).Err()
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Internal, "list Caddy certificates: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &pb.ListCertificatesResponse{}
|
||||||
|
for _, record := range records {
|
||||||
|
if err = ctx.Err(); err != nil {
|
||||||
|
return nil, status.FromContextError(err).Err()
|
||||||
|
}
|
||||||
|
if !isCertificateKey(record.Key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cert, err := s.loadIssuedCertificate(ctx, record.Key)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("Failed to load issued certificate from Caddy storage.", "key", record.Key, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resp.Certificates = append(resp.Certificates, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isCertificateKey recognizes CertMagic's certificates/<issuer>/<name>/<name>.crt layout.
|
||||||
|
func isCertificateKey(key string) bool {
|
||||||
|
parts := strings.Split(key, "/")
|
||||||
|
return len(parts) == 4 && parts[0] == "certificates" && parts[3] == parts[2]+".crt"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) loadIssuedCertificate(ctx context.Context, key string) (*pb.IssuedCertificate, error) {
|
||||||
|
chain, err := s.store.Get(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
metaKey := strings.TrimSuffix(key, ".crt") + ".json"
|
||||||
|
meta, err := s.store.Get(ctx, metaKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var resource certmagic.CertificateResource
|
||||||
|
if err = json.Unmarshal(meta.Value, &resource); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(resource.SANs) == 0 || strings.TrimSpace(resource.SANs[0]) == "" {
|
||||||
|
return nil, fmt.Errorf("certificate metadata has no SAN")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.IssuedCertificate{
|
||||||
|
San: resource.SANs[0],
|
||||||
|
Chain: chain.Value,
|
||||||
|
IssuerData: resource.IssuerData,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package caddystorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServerListCertificates(t *testing.T) {
|
||||||
|
for _, sans := range [][]string{
|
||||||
|
{"app.example.com"}, {"*.example.com"}, {"192.0.2.1"}, {"www.example.com", "example.com"},
|
||||||
|
} {
|
||||||
|
t.Run(sans[0], func(t *testing.T) {
|
||||||
|
bundle, _, _ := certificateFixture(t, &x509.Certificate{
|
||||||
|
DNSNames: []string{"example.com", "www.example.com"},
|
||||||
|
IPAddresses: []net.IP{net.ParseIP("192.0.2.1")},
|
||||||
|
NotAfter: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
})
|
||||||
|
key := certmagic.StorageKeys.SiteCert("local", sans[0])
|
||||||
|
storage := newCertificateStorage(t)
|
||||||
|
raw := json.RawMessage(`{"unknown_issuer_field":true}`)
|
||||||
|
storage.add(t, key, bundle, sans, raw)
|
||||||
|
storage.keys = append(storage.keys, "certificates", "certificates/local", "certificates/local/app.crt",
|
||||||
|
"certificates/local/app/wrong.crt", "certificates/local/app/app.crt/extra.crt",
|
||||||
|
"pki/authorities/local/root.crt", "ocsp/certificate")
|
||||||
|
|
||||||
|
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, resp.Certificates, 1)
|
||||||
|
cert := resp.Certificates[0]
|
||||||
|
assert.Equal(t, sans[0], cert.San)
|
||||||
|
assert.Equal(t, bundle, cert.Chain, "PEM must be returned unchanged")
|
||||||
|
assert.Equal(t, []byte(raw), cert.IssuerData)
|
||||||
|
assert.Equal(t, []string{key, strings.TrimSuffix(key, ".crt") + ".json"}, storage.loads)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerListCertificates_PreservesIssuerData(t *testing.T) {
|
||||||
|
bundle, _, _ := certificateFixture(t, &x509.Certificate{})
|
||||||
|
for _, raw := range []string{
|
||||||
|
"", "null", `{"url":"https://ca/cert/1","ca":"https://ca/directory"}`,
|
||||||
|
`{"url":"https://ca/cert/1","ca":"https://ca/directory","renewal_info":{"_selectedTime":"invalid"}}`,
|
||||||
|
`"provider-record"`, "[1,2,3]",
|
||||||
|
} {
|
||||||
|
t.Run(raw, func(t *testing.T) {
|
||||||
|
storage := newCertificateStorage(t)
|
||||||
|
storage.add(t, "certificates/custom/app/app.crt", bundle, []string{"app"}, json.RawMessage(raw))
|
||||||
|
|
||||||
|
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, resp.Certificates, 1)
|
||||||
|
assert.Equal(t, raw, string(resp.Certificates[0].IssuerData))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerListCertificates_OrderingAndDuplicates(t *testing.T) {
|
||||||
|
bundle, _, _ := certificateFixture(t, &x509.Certificate{})
|
||||||
|
storage := newCertificateStorage(t)
|
||||||
|
storage.add(t, certmagic.StorageKeys.SiteCert("z-issuer", "a.example.com"), bundle, []string{"a.example.com"}, nil)
|
||||||
|
storage.add(t, certmagic.StorageKeys.SiteCert("a-issuer", "z.example.com"), bundle, []string{"z.example.com"}, nil)
|
||||||
|
storage.add(t, certmagic.StorageKeys.SiteCert("b-issuer", "z.example.com"), bundle, []string{"z.example.com"}, nil)
|
||||||
|
|
||||||
|
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, resp.Certificates, 3)
|
||||||
|
certs := resp.Certificates
|
||||||
|
assert.Equal(t, []string{"z.example.com", "z.example.com", "a.example.com"},
|
||||||
|
[]string{certs[0].San, certs[1].San, certs[2].San})
|
||||||
|
for _, cert := range certs {
|
||||||
|
assert.Equal(t, bundle, cert.Chain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerListCertificates_SkipsUnreadableEntries(t *testing.T) {
|
||||||
|
bundle, _, _ := certificateFixture(t, &x509.Certificate{})
|
||||||
|
const badKey = "certificates/local/bad/bad.crt"
|
||||||
|
const metaKey = "certificates/local/bad/bad.json"
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
change func(*certificateStorageStub)
|
||||||
|
}{
|
||||||
|
{"missing chain", func(s *certificateStorageStub) { delete(s.values, badKey) }},
|
||||||
|
{"chain read failure", func(s *certificateStorageStub) { s.loadErrors[badKey] = errors.New("offline") }},
|
||||||
|
{"missing metadata", func(s *certificateStorageStub) { delete(s.values, metaKey) }},
|
||||||
|
{"metadata read failure", func(s *certificateStorageStub) { s.loadErrors[metaKey] = errors.New("offline") }},
|
||||||
|
{"invalid JSON", func(s *certificateStorageStub) { s.values[metaKey] = []byte("{") }},
|
||||||
|
{"missing SAN", func(s *certificateStorageStub) { s.values[metaKey] = []byte(`{}`) }},
|
||||||
|
{"empty first SAN", func(s *certificateStorageStub) { s.values[metaKey] = []byte(`{"sans":["","app"]}`) }},
|
||||||
|
{"blank first SAN", func(s *certificateStorageStub) { s.values[metaKey] = []byte(`{"sans":[" "]}`) }},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
storage := newCertificateStorage(t)
|
||||||
|
storage.add(t, badKey, bundle, []string{"bad"}, nil)
|
||||||
|
storage.add(t, "certificates/local/good/good.crt", bundle, []string{"good"}, nil)
|
||||||
|
tt.change(storage)
|
||||||
|
|
||||||
|
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, resp.Certificates, 1)
|
||||||
|
assert.Equal(t, "good", resp.Certificates[0].San)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("all entries unreadable", func(t *testing.T) {
|
||||||
|
storage := newCertificateStorage(t)
|
||||||
|
storage.keys = []string{badKey}
|
||||||
|
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, resp.Certificates)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerListCertificates_ListErrors(t *testing.T) {
|
||||||
|
for _, fail := range []bool{false, true} {
|
||||||
|
storage := newCertificateStorage(t)
|
||||||
|
if fail {
|
||||||
|
storage.listError = errors.New("offline")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
|
||||||
|
assert.Empty(t, storage.loads)
|
||||||
|
if fail {
|
||||||
|
assert.Nil(t, resp)
|
||||||
|
assert.Equal(t, codes.Internal, status.Code(err))
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, resp.Certificates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only reads are implemented. Unexpected writes panic through the nil embedded Keyspace.
|
||||||
|
type certificateStorageStub struct {
|
||||||
|
*store.Keyspace
|
||||||
|
t *testing.T
|
||||||
|
keys []string
|
||||||
|
values map[string][]byte
|
||||||
|
loads []string
|
||||||
|
listCalls int
|
||||||
|
listError error
|
||||||
|
loadErrors map[string]error
|
||||||
|
onLoad func(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCertificateStorage(t *testing.T) *certificateStorageStub {
|
||||||
|
return &certificateStorageStub{t: t, values: make(map[string][]byte), loadErrors: make(map[string]error)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *certificateStorageStub) add(t *testing.T, key string, bundle []byte, sans []string, issuerData json.RawMessage) {
|
||||||
|
t.Helper()
|
||||||
|
metaKey := strings.TrimSuffix(key, ".crt") + ".json"
|
||||||
|
keyKey := strings.TrimSuffix(key, ".crt") + ".key"
|
||||||
|
meta, err := json.Marshal(certmagic.CertificateResource{SANs: sans, IssuerData: issuerData})
|
||||||
|
require.NoError(t, err)
|
||||||
|
s.keys = append(s.keys, key, metaKey, keyKey)
|
||||||
|
s.values[key], s.values[metaKey], s.values[keyKey] = bundle, meta, []byte("must not read private keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *certificateStorageStub) List(ctx context.Context, prefix string, opts store.KeyspaceListOptions) ([]store.Record, error) {
|
||||||
|
s.listCalls++
|
||||||
|
assert.Equal(s.t, "certificates/", prefix)
|
||||||
|
assert.True(s.t, opts.KeysOnly, "listing must not fetch private key values")
|
||||||
|
var records []store.Record
|
||||||
|
for _, key := range slices.Sorted(slices.Values(s.keys)) {
|
||||||
|
records = append(records, store.Record{Key: key})
|
||||||
|
}
|
||||||
|
return records, s.listError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *certificateStorageStub) Get(ctx context.Context, key string) (store.Record, error) {
|
||||||
|
require.False(s.t, strings.HasSuffix(key, ".key"), "private keys must never be loaded")
|
||||||
|
s.loads = append(s.loads, key)
|
||||||
|
if s.onLoad != nil {
|
||||||
|
s.onLoad(key)
|
||||||
|
}
|
||||||
|
if err := s.loadErrors[key]; err != nil {
|
||||||
|
return store.Record{}, err
|
||||||
|
}
|
||||||
|
value, ok := s.values[key]
|
||||||
|
if !ok {
|
||||||
|
return store.Record{}, store.ErrKeyNotFound
|
||||||
|
}
|
||||||
|
return store.Record{Key: key, Value: value}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func certificateFixture(t *testing.T, template *x509.Certificate) ([]byte, *x509.Certificate, *x509.Certificate) {
|
||||||
|
t.Helper()
|
||||||
|
publicKey, key, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
root := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "Test CA"}, IsCA: true,
|
||||||
|
BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign,
|
||||||
|
NotBefore: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
NotAfter: time.Date(2040, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
}
|
||||||
|
rootDER, err := x509.CreateCertificate(rand.Reader, root, root, publicKey, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
root, err = x509.ParseCertificate(rootDER)
|
||||||
|
require.NoError(t, err)
|
||||||
|
template.SerialNumber = big.NewInt(2)
|
||||||
|
template.NotBefore = root.NotBefore
|
||||||
|
if template.NotAfter.IsZero() {
|
||||||
|
template.NotAfter = root.NotAfter
|
||||||
|
}
|
||||||
|
leafDER, err := x509.CreateCertificate(rand.Reader, template, root, publicKey, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
leaf, err := x509.ParseCertificate(leafDER)
|
||||||
|
require.NoError(t, err)
|
||||||
|
bundle := append(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafDER}),
|
||||||
|
pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootDER})...)
|
||||||
|
return bundle, leaf, root
|
||||||
|
}
|
||||||
@@ -25,10 +25,17 @@ const Namespace = "caddy_storage"
|
|||||||
// Server implements the machine-local CaddyStorage gRPC service.
|
// Server implements the machine-local CaddyStorage gRPC service.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
pb.UnimplementedCaddyStorageServer
|
pb.UnimplementedCaddyStorageServer
|
||||||
store *store.Keyspace
|
store keyspace
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(store *store.Keyspace) *Server {
|
type keyspace interface {
|
||||||
|
Get(context.Context, string) (store.Record, error)
|
||||||
|
Put(context.Context, string, []byte) error
|
||||||
|
Delete(context.Context, string, store.KeyspaceDeleteOptions) error
|
||||||
|
List(context.Context, string, store.KeyspaceListOptions) ([]store.Record, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(store keyspace) *Server {
|
||||||
return &Server{store: store}
|
return &Server{store: store}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/psviderski/uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
"github.com/psviderski/uncloud/internal/machine/store"
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
"github.com/psviderski/uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
@@ -87,6 +88,11 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
|
|||||||
func (c *Cluster) AddMachineWithoutReadyCheck(
|
func (c *Cluster) AddMachineWithoutReadyCheck(
|
||||||
ctx context.Context, req *pb.AddMachineRequest,
|
ctx context.Context, req *pb.AddMachineRequest,
|
||||||
) (*pb.AddMachineResponse, error) {
|
) (*pb.AddMachineResponse, error) {
|
||||||
|
if req.Name != "" {
|
||||||
|
if err := api.ValidateMachineName(req.Name); err != nil {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
if req.Network == nil {
|
if req.Network == nil {
|
||||||
return nil, status.Error(codes.InvalidArgument, "network not set")
|
return nil, status.Error(codes.InvalidArgument, "network not set")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/api/pb"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAddMachine_InvalidName(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ready := make(chan struct{})
|
||||||
|
close(ready)
|
||||||
|
// Invalid names must be rejected before any store access.
|
||||||
|
c := NewCluster(nil, nil, nil, ready)
|
||||||
|
for _, name := range []string{"VPS1", "worker.example", "rr", "nearest", "c337f00600de51ef4375c9a9a267dba5"} {
|
||||||
|
t.Run("add", func(t *testing.T) {
|
||||||
|
resp, err := c.AddMachine(context.Background(), &pb.AddMachineRequest{Name: name})
|
||||||
|
require.Nil(t, resp)
|
||||||
|
require.Equal(t, codes.InvalidArgument, status.Code(err))
|
||||||
|
require.ErrorContains(t, err, "invalid machine name")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,20 +4,24 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
"github.com/psviderski/uncloud/api/pb"
|
"github.com/psviderski/uncloud/api/pb"
|
||||||
"github.com/psviderski/uncloud/internal/dns"
|
undns "github.com/psviderski/uncloud/internal/dns"
|
||||||
"github.com/psviderski/uncloud/internal/machine/store"
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// uncloudDNSKey is the key used to store the details of the reserved domain in the store.
|
// uncloudDNSKey stores the cluster domain and, when reserved, its Uncloud DNS credentials.
|
||||||
const uncloudDNSKey = "uncloud_dns"
|
const uncloudDNSKey = "uncloud_dns"
|
||||||
|
|
||||||
type uncloudDNSDomain struct {
|
type uncloudDNSDomain struct {
|
||||||
// Endpoint is the API endpoint of the Uncloud DNS service where the domain is reserved.
|
// Endpoint is the API endpoint of the Uncloud DNS service where the domain is reserved.
|
||||||
|
// An empty endpoint means the domain is managed externally.
|
||||||
Endpoint string
|
Endpoint string
|
||||||
Name string
|
Name string
|
||||||
// TODO: encrypt the token in the store.
|
// TODO: encrypt the token in the store.
|
||||||
@@ -34,14 +38,14 @@ func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainReques
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := c.storedDomain(ctx); err == nil {
|
if _, err := c.storedDomain(ctx); err == nil {
|
||||||
return nil, status.Errorf(codes.AlreadyExists, "domain already reserved")
|
return nil, status.Error(codes.AlreadyExists, "cluster domain already configured")
|
||||||
} else {
|
} else {
|
||||||
if s := status.Convert(err); s.Code() != codes.NotFound {
|
if s := status.Convert(err); s.Code() != codes.NotFound {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsClient := dns.NewClient()
|
dnsClient := undns.NewClient()
|
||||||
name, token, err := dnsClient.ReserveDomain(req.Endpoint)
|
name, token, err := dnsClient.ReserveDomain(req.Endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Error(codes.Internal, err.Error())
|
return nil, status.Error(codes.Internal, err.Error())
|
||||||
@@ -60,7 +64,7 @@ func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainReques
|
|||||||
return nil, status.Errorf(codes.Internal, "store reserved domain: %v", err)
|
return nil, status.Errorf(codes.Internal, "store reserved domain: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &pb.Domain{Name: name}, nil
|
return &pb.Domain{Name: name, Reserved: proto.Bool(true)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
|
func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
|
||||||
@@ -73,7 +77,7 @@ func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &pb.Domain{Name: domain.Name}, nil
|
return &pb.Domain{Name: domain.Name, Reserved: proto.Bool(domain.Endpoint != "")}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) storedDomain(ctx context.Context) (uncloudDNSDomain, error) {
|
func (c *Cluster) storedDomain(ctx context.Context) (uncloudDNSDomain, error) {
|
||||||
@@ -103,13 +107,63 @@ func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Doma
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if domain.Endpoint == "" {
|
||||||
|
return nil, status.Error(codes.FailedPrecondition,
|
||||||
|
"cluster domain is set manually, use 'uc dns set \"\"' to unset it")
|
||||||
|
}
|
||||||
|
|
||||||
if err = c.store.Delete(ctx, uncloudDNSKey); err != nil {
|
if err = c.store.Delete(ctx, uncloudDNSKey); err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "delete domain from store: %v", err)
|
return nil, status.Errorf(codes.Internal, "delete domain from store: %v", err)
|
||||||
}
|
}
|
||||||
// TODO: implement and call Uncloud DNS endpoint to release/delete the domain.
|
// TODO: implement and call Uncloud DNS endpoint to release/delete the domain.
|
||||||
|
|
||||||
return &pb.Domain{Name: domain.Name}, nil
|
return &pb.Domain{Name: domain.Name, Reserved: proto.Bool(true)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) SetDomain(ctx context.Context, req *pb.SetDomainRequest) (*emptypb.Empty, error) {
|
||||||
|
if err := c.checkReady(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
name := req.GetName()
|
||||||
|
if name != "" {
|
||||||
|
if labels, ok := dns.IsDomainName(name); !ok || labels < 2 {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument,
|
||||||
|
"invalid cluster domain '%s': must be a valid domain name with at least two labels", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name = strings.ToLower(strings.TrimSuffix(name, "."))
|
||||||
|
|
||||||
|
existing, err := c.storedDomain(ctx)
|
||||||
|
if err != nil && status.Code(err) != codes.NotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
if err == nil {
|
||||||
|
if existing.Endpoint != "" {
|
||||||
|
return nil, status.Error(codes.FailedPrecondition,
|
||||||
|
"cluster domain is reserved in Uncloud DNS, use 'uc dns release' to release it")
|
||||||
|
}
|
||||||
|
if err := c.store.Delete(ctx, uncloudDNSKey); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "unset cluster domain: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &emptypb.Empty{}, nil
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
return nil, status.Error(codes.AlreadyExists, "cluster domain already configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
domain := uncloudDNSDomain{Name: name}
|
||||||
|
domainJSON, err := json.Marshal(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "marshal set domain for store: %v", err)
|
||||||
|
}
|
||||||
|
if err = c.store.Put(ctx, uncloudDNSKey, domainJSON); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "store set domain: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &emptypb.Empty{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) CreateDomainRecords(
|
func (c *Cluster) CreateDomainRecords(
|
||||||
@@ -124,12 +178,16 @@ func (c *Cluster) CreateDomainRecords(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsClient := dns.NewClient()
|
if domain.Endpoint == "" {
|
||||||
recordsReq := make([]dns.RecordRequest, len(req.Records))
|
return nil, status.Error(codes.FailedPrecondition, "cluster domain is not reserved in Uncloud DNS")
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsClient := undns.NewClient()
|
||||||
|
recordsReq := make([]undns.RecordRequest, len(req.Records))
|
||||||
for i, r := range req.Records {
|
for i, r := range req.Records {
|
||||||
recordsReq[i] = dns.RecordRequest{
|
recordsReq[i] = undns.RecordRequest{
|
||||||
Name: r.Name,
|
Name: r.Name,
|
||||||
Type: dns.RecordType(r.Type.String()),
|
Type: undns.RecordType(r.Type.String()),
|
||||||
Values: r.Values,
|
Values: r.Values,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,9 +207,9 @@ func (c *Cluster) CreateDomainRecords(
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch r.Type {
|
switch r.Type {
|
||||||
case dns.RecordTypeA:
|
case undns.RecordTypeA:
|
||||||
resp.Records[i].Type = pb.DNSRecord_A
|
resp.Records[i].Type = pb.DNSRecord_A
|
||||||
case dns.RecordTypeAAAA:
|
case undns.RecordTypeAAAA:
|
||||||
resp.Records[i].Type = pb.DNSRecord_AAAA
|
resp.Records[i].Type = pb.DNSRecord_AAAA
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user