├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── bin └── .gitkeep ├── cmd └── cloud-run-api-emulator │ └── main.go ├── compose.yaml ├── go.mod ├── go.sum ├── internal ├── command │ └── command.go ├── domain │ └── services.go ├── e2etest │ ├── e2etest_test.go │ ├── services_test.go │ └── testdata │ │ └── seed.yaml ├── handler │ ├── db │ │ ├── sqlite │ │ │ ├── queries │ │ │ │ ├── list_service_annotations.sql │ │ │ │ ├── list_services.sql │ │ │ │ └── list_services_by_created_at_and_name.sql │ │ │ ├── schema.sql │ │ │ ├── services_repository.go │ │ │ ├── sqlite.go │ │ │ └── xo │ │ │ │ ├── db.xo.go │ │ │ │ ├── listnextservices.xo.go │ │ │ │ ├── listserviceannotations.xo.go │ │ │ │ ├── listservices.xo.go │ │ │ │ ├── service.xo.go │ │ │ │ ├── serviceannotation.xo.go │ │ │ │ └── servicelabel.xo.go │ │ └── yaml │ │ │ ├── testdata │ │ │ └── seed.yaml │ │ │ ├── yaml.go │ │ │ └── yaml_test.go │ └── grpc │ │ ├── executions_server.go │ │ ├── grpc.go │ │ ├── jobs_server.go │ │ ├── revisions_server.go │ │ ├── services_server.go │ │ └── tasks_server.go └── usecase │ └── services_usecase.go └── tools ├── go.mod ├── go.sum └── tools.go /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !/cmd 4 | !/internal 5 | !/go.mod 6 | !/go.sum 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /.env 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20.4-bullseye AS builder 2 | 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | ENV GOOS=${TARGETOS} 7 | ENV GOARCH=${TARGETARCH} 8 | 9 | WORKDIR /go/src/github.com/kauche/cloud-run-api-emulator 10 | 11 | COPY . . 12 | 13 | RUN CGO_ENABLED=0 go build \ 14 | -a \ 15 | -trimpath \ 16 | -ldflags "-s -w -extldflags -static" \ 17 | -o /usr/bin/cloud-run-api-emulator \ 18 | ./cmd/cloud-run-api-emulator 19 | 20 | FROM gcr.io/distroless/base@sha256:df13a91fd415eb192a75e2ef7eacf3bb5877bb05ce93064b91b83feef5431f37 21 | COPY --from=builder /usr/bin/cloud-run-api-emulator /usr/bin/cloud-run-api-emulator 22 | 23 | CMD ["/usr/bin/cloud-run-api-emulator"] 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 KAUCHE, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILD_TOOLS := cd ./tools && go build -o 2 | 3 | BIN_DIR := ./bin 4 | 5 | XO := $(abspath $(BIN_DIR)/xo) 6 | 7 | .PHONY: xo 8 | xo: 9 | @$(BUILD_TOOLS) $(XO) github.com/xo/xo 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloud Run API Emulator 2 | 3 | #### ⚠️ This project is still in the super experimental phase ⚠️ 4 | 5 | Cloud Run API Emulator provides application developers with a locally-running, emulated instance of Cloud Run API to enable local development and testing. 6 | 7 | ## Usage 8 | 9 | Run cloud-run-api-emulator by using docker like below: 10 | 11 | ``` 12 | $ docker run --publish 8000:8000 --detach ghcr.io/kauche/cloud-run-api-emulator:0.0.3 13 | ``` 14 | 15 | And then, you can use the emulator through a gRPC client like below: 16 | 17 | (The example below is written in Go, but you can use any language.) 18 | 19 | ```go 20 | package main 21 | 22 | import ( 23 | "context" 24 | "fmt" 25 | "os" 26 | 27 | "cloud.google.com/go/run/apiv2/runpb" 28 | "google.golang.org/grpc" 29 | "google.golang.org/grpc/credentials/insecure" 30 | ) 31 | 32 | func main() { 33 | ctx := context.Background() 34 | 35 | cc, err := grpc.DialContext(ctx, "localhost:8000", grpc.WithTransportCredentials(insecure.NewCredentials())) 36 | if err != nil { 37 | fmt.Fprintf(os.Stderr, "failed to connect to the emulator: %s\n", err) 38 | os.Exit(1) 39 | } 40 | defer func() { 41 | if err = cc.Close(); err != nil { 42 | fmt.Fprintf(os.Stderr, "failed to close the connection: %s\n", err) 43 | os.Exit(1) 44 | } 45 | }() 46 | 47 | client := runpb.NewServicesClient(cc) 48 | 49 | createReq := &runpb.CreateServiceRequest{ 50 | Parent: "projects/test-project/locations/us-central1", 51 | ServiceId: "my-service", 52 | Service: &runpb.Service{ 53 | Description: "my service", 54 | }, 55 | } 56 | 57 | _, err = client.CreateService(ctx, createReq) 58 | if err != nil { 59 | fmt.Fprintf(os.Stderr, "failed to create the service: %s\n", err) 60 | os.Exit(1) 61 | } 62 | 63 | listReq := &runpb.ListServicesRequest{ 64 | Parent: "projects/test-project/locations/us-central1", 65 | PageSize: 5, 66 | } 67 | 68 | listRes, err := client.ListServices(ctx, listReq) 69 | if err != nil { 70 | fmt.Fprintf(os.Stderr, "failed to list services: %s\n", err) 71 | os.Exit(1) 72 | } 73 | 74 | // this prints `service: projects/test-project/locations/us-central1/services/my-service` 75 | for _, service := range listRes.Services { 76 | fmt.Printf("service: %s\n", service.Name) 77 | } 78 | } 79 | 80 | ``` 81 | 82 | ## Supported Methods 83 | 84 | ### Services 85 | 86 | - [x] CreateService 87 | - ValidateOnly mode is not yet supported. 88 | - Some fields of Service are not yet supported. 89 | - [ ] GetService 90 | - [x] ListServices 91 | - ShowDeleted is not yet supported. 92 | - Some fields of Service are not yet supported. 93 | - [ ] UpdateService 94 | - [ ] DeleteService 95 | - [ ] GetIamPolicy 96 | - [ ] SetIamPolicy 97 | - [ ] TestIamPermissions 98 | 99 | ### Revisions 100 | 101 | - [ ] GetRevision 102 | - [ ] ListRevisions 103 | - [ ] DeleteRevision 104 | 105 | ### Jobs 106 | 107 | - [ ] CreateJob 108 | - [ ] GetJob 109 | - [ ] ListJobs 110 | - [ ] UpdateJob 111 | - [ ] DeleteJob 112 | - [ ] RunJob 113 | - [ ] GetIamPolicy 114 | - [ ] SetIamPolicy 115 | - [ ] TestIamPermissions 116 | 117 | ### Tasks 118 | 119 | - [ ] GetTask 120 | - [ ] ListTasks 121 | 122 | ### Executions 123 | 124 | - [ ] GetExecution 125 | - [ ] ListExecutions 126 | - [ ] DeleteExecution 127 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kauche/cloud-run-api-emulator/f77c38b0b4722bd9d50b02f7939456571eff888c/bin/.gitkeep -------------------------------------------------------------------------------- /cmd/cloud-run-api-emulator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/kauche/cloud-run-api-emulator/internal/command" 4 | 5 | func main() { 6 | command.Run() 7 | } 8 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | 4 | emulator: 5 | image: golang:1.20.4-bullseye 6 | ports: 7 | - ${PORT-8000}:8000 8 | volumes: 9 | - .:/go/src/github.com/kauche/cloud-run-api-emulator:cached 10 | - go-pkg-mod:/go/pkg/mod:cached 11 | - ${GOCACHE:-~/.cache/go-build}:/tmp/go-build 12 | working_dir: /go/src/github.com/kauche/cloud-run-api-emulator 13 | command: go run -ldflags "-s -w -extldflags -static" ./cmd/cloud-run-api-emulator --data ./bin/cloud-run-api-emulator.db --seed ./internal/e2etest/testdata/seed.yaml 14 | environment: 15 | CGO_ENABLED: 0 16 | GOCACHE: /tmp/go-build 17 | 18 | volumes: 19 | go-pkg-mod: 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kauche/cloud-run-api-emulator 2 | 3 | go 1.20 4 | 5 | require ( 6 | cloud.google.com/go/longrunning v0.4.1 7 | cloud.google.com/go/run v1.0.1 8 | github.com/110y/run v1.0.2 9 | github.com/110y/servergroup v0.2.1 10 | github.com/glebarez/go-sqlite v1.21.1 11 | github.com/goccy/go-yaml v1.11.0 12 | github.com/google/go-cmp v0.5.9 13 | github.com/google/uuid v1.3.0 14 | github.com/kauche/bjt v0.1.0 15 | google.golang.org/grpc v1.55.0 16 | google.golang.org/protobuf v1.30.0 17 | ) 18 | 19 | require ( 20 | cloud.google.com/go/iam v0.13.0 // indirect 21 | github.com/dustin/go-humanize v1.0.1 // indirect 22 | github.com/fatih/color v1.10.0 // indirect 23 | github.com/golang/protobuf v1.5.3 // indirect 24 | github.com/mattn/go-colorable v0.1.8 // indirect 25 | github.com/mattn/go-isatty v0.0.17 // indirect 26 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 27 | golang.org/x/net v0.9.0 // indirect 28 | golang.org/x/sys v0.8.0 // indirect 29 | golang.org/x/text v0.9.0 // indirect 30 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 31 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 32 | modernc.org/libc v1.22.3 // indirect 33 | modernc.org/mathutil v1.5.0 // indirect 34 | modernc.org/memory v1.5.0 // indirect 35 | modernc.org/sqlite v1.21.1 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= 2 | cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= 3 | cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= 4 | cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= 5 | cloud.google.com/go/run v1.0.1 h1:Qk3CWIVtuvtpQxYaWmQby8W7b+FVpGvxZo8yh2B67tw= 6 | cloud.google.com/go/run v1.0.1/go.mod h1:x1TUM+zJOM0FAeg8vKPJDXqnj1Jj3nUIAeiSyxiBmoY= 7 | github.com/110y/run v1.0.2 h1:wMkLlIeMFm/YuXut9T2Rrcorj+C7NFPhMYM52dzsQMg= 8 | github.com/110y/run v1.0.2/go.mod h1:HFz7xuBNEmYHtb+cvqJP4QtOSFKzKQRus0qv29aGYig= 9 | github.com/110y/servergroup v0.2.1 h1:8oVFPpsOkDVZ4fLfWnDv9KYJS1hXuQzH/XFaeE53eoY= 10 | github.com/110y/servergroup v0.2.1/go.mod h1:P5/1RK7u7zbhLrhQkUudNJlkKjT70J7d/0So50QKVFQ= 11 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 12 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 13 | github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= 14 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 15 | github.com/glebarez/go-sqlite v1.21.1 h1:7MZyUPh2XTrHS7xNEHQbrhfMZuPSzhkm2A1qgg0y5NY= 16 | github.com/glebarez/go-sqlite v1.21.1/go.mod h1:ISs8MF6yk5cL4n/43rSOmVMGJJjHYr7L2MbZZ5Q4E2E= 17 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 18 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 19 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 20 | github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= 21 | github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= 22 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 23 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 24 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 25 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 26 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 27 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 28 | github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= 29 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 30 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 31 | github.com/kauche/bjt v0.1.0 h1:Bec8FdpnJ5b2/3m5HygOyAqkUnhp04t1HTx+Gj4jmag= 32 | github.com/kauche/bjt v0.1.0/go.mod h1:VISiDYZxxViMeGk0b0OCePk8gcyQp4PPB7669onXdcA= 33 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 34 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= 35 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 36 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 37 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 38 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 39 | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 40 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 41 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 42 | golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= 43 | golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= 44 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 45 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 46 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 47 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 48 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 49 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 50 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 51 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 52 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 53 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 54 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 55 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= 56 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= 57 | google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= 58 | google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= 59 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 60 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 61 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 62 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 63 | modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY= 64 | modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw= 65 | modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= 66 | modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= 67 | modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= 68 | modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= 69 | modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU= 70 | modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI= 71 | -------------------------------------------------------------------------------- /internal/command/command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/110y/run" 10 | "github.com/110y/servergroup" 11 | 12 | "github.com/kauche/cloud-run-api-emulator/internal/handler/db/sqlite" 13 | "github.com/kauche/cloud-run-api-emulator/internal/handler/db/yaml" 14 | "github.com/kauche/cloud-run-api-emulator/internal/handler/grpc" 15 | "github.com/kauche/cloud-run-api-emulator/internal/usecase" 16 | ) 17 | 18 | func Run() { 19 | run.Run(server) 20 | } 21 | 22 | func server(ctx context.Context) (code int) { 23 | data := flag.String("data", "", "A database file path to persist the data") 24 | seed := flag.String("seed", "", "A path to a YAML file that contains the seed data") 25 | 26 | flag.Parse() 27 | 28 | if *data == "" { 29 | *data = ":memory:" 30 | } 31 | 32 | db, err := sqlite.NewDB(*data) 33 | if err != nil { 34 | // TODO: structured log 35 | fmt.Fprintf(os.Stderr, "failed to create the database connection: %s\n", err) 36 | return 1 37 | } 38 | defer func() { 39 | if err := db.Close(); err != nil { 40 | // TODO: structured log 41 | fmt.Fprintf(os.Stderr, "failed to close the database connection: %s\n", err) 42 | code = 1 43 | } 44 | }() 45 | 46 | srepo := sqlite.NewServicesRepository(db) 47 | suc := usecase.NewServicesUsecase(srepo) 48 | 49 | gs := grpc.NewServer(8000, suc) // TODO: make the port configurable 50 | 51 | if *seed != "" { 52 | seedServices, err := yaml.GetSeeds(*seed) 53 | if err != nil { 54 | // TODO: structured log 55 | fmt.Fprintf(os.Stderr, "failed to read the seed file: %s\n", err) 56 | return 0 57 | } 58 | 59 | if err := srepo.CreateServices(ctx, seedServices); err != nil { 60 | // TODO: structured log 61 | fmt.Fprintf(os.Stderr, "failed to save the seed services: %s\n", err) 62 | return 0 63 | } 64 | } 65 | 66 | var sg servergroup.Group 67 | sg.Add(gs) 68 | 69 | if err := sg.Start(ctx); err != nil { 70 | // TODO: structured log 71 | fmt.Fprintf(os.Stderr, "the server aborted: %s\n", err) 72 | return 1 73 | } 74 | 75 | return 0 76 | } 77 | -------------------------------------------------------------------------------- /internal/domain/services.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | 6 | "cloud.google.com/go/run/apiv2/runpb" 7 | ) 8 | 9 | type ServicesRepository interface { 10 | CreateService(ctx context.Context, parent string, service *runpb.Service) error 11 | CreateServices(ctx context.Context, service []*runpb.Service) error 12 | ListServices(ctx context.Context, parent string, limit int32) ([]*runpb.Service, error) 13 | ListServicesByParentCreatedAtName(ctx context.Context, parent, createdAt, name string, limit int32) ([]*runpb.Service, error) 14 | } 15 | -------------------------------------------------------------------------------- /internal/e2etest/e2etest_test.go: -------------------------------------------------------------------------------- 1 | package e2etest 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "cloud.google.com/go/run/apiv2/runpb" 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/credentials/insecure" 12 | ) 13 | 14 | func TestMain(m *testing.M) { 15 | os.Exit(func() int { 16 | ctx := context.Background() 17 | 18 | target := os.Getenv("EMULATOR_TARGET") 19 | if target == "" { 20 | fmt.Fprintln(os.Stderr, "the emulator target is not set") 21 | return 1 22 | } 23 | 24 | cc, err := grpc.DialContext(ctx, target, grpc.WithTransportCredentials(insecure.NewCredentials())) 25 | if err != nil { 26 | fmt.Fprintf(os.Stderr, "failed to connect to the emulator: %s\n", err) 27 | return 1 28 | } 29 | 30 | serviceClient = runpb.NewServicesClient(cc) 31 | 32 | return m.Run() 33 | }()) 34 | } 35 | -------------------------------------------------------------------------------- /internal/e2etest/services_test.go: -------------------------------------------------------------------------------- 1 | package e2etest 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "cloud.google.com/go/longrunning/autogen/longrunningpb" 9 | "cloud.google.com/go/run/apiv2/runpb" 10 | "github.com/google/go-cmp/cmp" 11 | "github.com/google/uuid" 12 | "google.golang.org/protobuf/testing/protocmp" 13 | 14 | "github.com/kauche/cloud-run-api-emulator/internal/handler/db/sqlite" 15 | ) 16 | 17 | var serviceClient runpb.ServicesClient 18 | 19 | func TestServices(t *testing.T) { 20 | t.Parallel() 21 | 22 | project := uuid.New().String() 23 | parent := fmt.Sprintf("projects/%s/locations/us-central1", project) 24 | 25 | t.Cleanup(func() { 26 | db, err := sqlite.NewDB("../../bin/cloud-run-api-emulator.db") 27 | if err != nil { 28 | t.Errorf("failed to create the database connection for the clean up: %s", err) 29 | return 30 | } 31 | defer db.Close() 32 | 33 | _, err = db.Exec("DELETE FROM services WHERE parent = $1", parent) 34 | if err != nil { 35 | t.Errorf("failed to delete services for the celan up: %s", err) 36 | return 37 | } 38 | }) 39 | 40 | ctx := context.Background() 41 | 42 | numServices := 10 43 | pageSize := 2 44 | 45 | for i := 1; i <= numServices; i++ { 46 | req := &runpb.CreateServiceRequest{ 47 | Parent: parent, 48 | ServiceId: fmt.Sprintf("test-service-%d", i), 49 | Service: &runpb.Service{ 50 | Description: fmt.Sprintf("test service %d", i), 51 | Uri: fmt.Sprintf("service-%d.example.com", i), 52 | Annotations: map[string]string{ 53 | "annotation-key-1": "annotation-value-1", 54 | "annotation-key-2": "annotation-value-2", 55 | }, 56 | }, 57 | } 58 | 59 | got, err := serviceClient.CreateService(ctx, req) 60 | if err != nil { 61 | t.Errorf("failed to create service: %s", err) 62 | return 63 | } 64 | 65 | want := &longrunningpb.Operation{ 66 | Done: true, 67 | } 68 | 69 | if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { 70 | t.Errorf("\n(-got, +want)\n%s", diff) 71 | return 72 | } 73 | } 74 | 75 | var pageToken string 76 | for i := 0; i < numServices/pageSize; i++ { 77 | req := &runpb.ListServicesRequest{ 78 | Parent: parent, 79 | PageSize: int32(pageSize), 80 | PageToken: pageToken, 81 | } 82 | 83 | res, err := serviceClient.ListServices(ctx, req) 84 | if err != nil { 85 | t.Errorf("failed to list services: %s", err) 86 | return 87 | } 88 | 89 | serviceNumber := (numServices/pageSize - i) * pageSize 90 | want := []*runpb.Service{ 91 | { 92 | Name: fmt.Sprintf("%s/services/test-service-%d", parent, serviceNumber), 93 | Description: fmt.Sprintf("test service %d", serviceNumber), 94 | Uri: fmt.Sprintf("service-%d.example.com", serviceNumber), 95 | Generation: 0, 96 | Annotations: map[string]string{ 97 | "annotation-key-1": "annotation-value-1", 98 | "annotation-key-2": "annotation-value-2", 99 | }, 100 | }, 101 | { 102 | Name: fmt.Sprintf("%s/services/test-service-%d", parent, serviceNumber-1), 103 | Description: fmt.Sprintf("test service %d", serviceNumber-1), 104 | Uri: fmt.Sprintf("service-%d.example.com", serviceNumber-1), 105 | Generation: 0, 106 | Annotations: map[string]string{ 107 | "annotation-key-1": "annotation-value-1", 108 | "annotation-key-2": "annotation-value-2", 109 | }, 110 | }, 111 | } 112 | 113 | if diff := cmp.Diff(res.Services, want, protocmp.IgnoreFields(&runpb.Service{}, "create_time"), protocmp.Transform()); diff != "" { 114 | t.Errorf("\n(-got, +want)\n%s", diff) 115 | return 116 | } 117 | 118 | pageToken = res.GetNextPageToken() 119 | } 120 | 121 | if pageToken != "" { 122 | t.Errorf("pageToken should be empty at the last page, but got %s", pageToken) 123 | } 124 | } 125 | 126 | func TestServices_Seed(t *testing.T) { 127 | ctx := context.Background() 128 | 129 | req := &runpb.ListServicesRequest{ 130 | Parent: "projects/test-project/locations/us-central1", 131 | PageSize: 5, 132 | } 133 | 134 | res, err := serviceClient.ListServices(ctx, req) 135 | if err != nil { 136 | t.Errorf("failed to list services: %s", err) 137 | return 138 | } 139 | 140 | want := []*runpb.Service{ 141 | { 142 | Name: "projects/test-project/locations/us-central1/services/service-1", 143 | Uid: "7de882f5-d2bc-4243-9fcd-90f982a7409e", 144 | Generation: 1, 145 | Annotations: map[string]string{ 146 | "annotation-1": "value-1", 147 | "annotation-2": "value-2", 148 | }, 149 | }, 150 | { 151 | Name: "projects/test-project/locations/us-central1/services/service-2", 152 | Uid: "a1e4b452-1737-418f-a4f9-34f128447391", 153 | Generation: 2, 154 | Annotations: map[string]string{ 155 | "annotation-1": "value-1", 156 | "annotation-2": "value-2", 157 | }, 158 | }, 159 | } 160 | 161 | if diff := cmp.Diff(res.Services, want, protocmp.IgnoreFields(&runpb.Service{}, "create_time"), protocmp.Transform()); diff != "" { 162 | t.Errorf("\n(-got, +want)\n%s", diff) 163 | return 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /internal/e2etest/testdata/seed.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | - name: projects/test-project/locations/us-central1/services/service-1 4 | uid: 7de882f5-d2bc-4243-9fcd-90f982a7409e 5 | generation: 1 6 | annotations: 7 | annotation-1: value-1 8 | annotation-2: value-2 9 | 10 | - name: projects/test-project/locations/us-central1/services/service-2 11 | uid: a1e4b452-1737-418f-a4f9-34f128447391 12 | generation: 2 13 | annotations: 14 | annotation-1: value-1 15 | annotation-2: value-2 16 | -------------------------------------------------------------------------------- /internal/handler/db/sqlite/queries/list_service_annotations.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | service_parent, 3 | service_name, 4 | key, 5 | value 6 | FROM service_annotations 7 | WHERE 8 | service_parent = %%parent string%% AND 9 | service_name = %%name string%% 10 | -------------------------------------------------------------------------------- /internal/handler/db/sqlite/queries/list_services.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | parent, 3 | name, 4 | description, 5 | uid, 6 | uri, 7 | generation, 8 | created_at 9 | FROM services 10 | WHERE 11 | parent = %%parent string%% 12 | ORDER BY 13 | created_at DESC, 14 | parent ASC, 15 | name ASC 16 | LIMIT 17 | %%limit int32%%; 18 | -------------------------------------------------------------------------------- /internal/handler/db/sqlite/queries/list_services_by_created_at_and_name.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | parent, 3 | name, 4 | description, 5 | uid, 6 | uri, 7 | generation, 8 | created_at 9 | FROM services 10 | WHERE 11 | parent = %%parent string%% AND 12 | created_at <= %%created_at string%% AND 13 | (created_at < %%created_at string%% OR name > %%name string%%) 14 | ORDER BY 15 | created_at DESC, 16 | parent ASC, 17 | name ASC 18 | LIMIT 19 | %%limit int32%%; 20 | -------------------------------------------------------------------------------- /internal/handler/db/sqlite/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS services ( 2 | parent TEXT NOT NULL, 3 | name TEXT NOT NULL, 4 | description TEXT NOT NULL, 5 | uid TEXT NOT NULL, 6 | generation BIGINT NOT NULL, 7 | uri TEXT NOT NULL, 8 | created_at TIMESTAMP NOT NULL, 9 | PRIMARY KEY (parent, name) 10 | ); 11 | 12 | CREATE INDEX IF NOT EXISTS created_at_desc ON services(created_at DESC); 13 | 14 | CREATE TABLE IF NOT EXISTS service_labels ( 15 | service_parent TEXT NOT NULL, 16 | service_name TEXT NOT NULL, 17 | key TEXT NOT NULL, 18 | value TEXT NOT NULL, 19 | PRIMARY KEY (service_parent, service_name, key) 20 | ); 21 | 22 | CREATE TABLE IF NOT EXISTS service_annotations ( 23 | service_parent TEXT NOT NULL, 24 | service_name TEXT NOT NULL, 25 | key TEXT NOT NULL, 26 | value TEXT NOT NULL, 27 | PRIMARY KEY (service_parent, service_name, key) 28 | ); 29 | -------------------------------------------------------------------------------- /internal/handler/db/sqlite/services_repository.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "strings" 8 | 9 | "cloud.google.com/go/run/apiv2/runpb" 10 | "google.golang.org/protobuf/types/known/timestamppb" 11 | 12 | "github.com/kauche/cloud-run-api-emulator/internal/domain" 13 | "github.com/kauche/cloud-run-api-emulator/internal/handler/db/sqlite/xo" 14 | ) 15 | 16 | var _ domain.ServicesRepository = (*ServicesRepository)(nil) 17 | 18 | func NewServicesRepository(db *sql.DB) *ServicesRepository { 19 | return &ServicesRepository{ 20 | db: db, 21 | } 22 | } 23 | 24 | type ServicesRepository struct { 25 | db *sql.DB 26 | } 27 | 28 | func (r *ServicesRepository) CreateService(ctx context.Context, parent string, service *runpb.Service) error { 29 | s := &xo.Service{ 30 | Parent: parent, 31 | Name: service.Name, 32 | Description: service.Description, 33 | URI: service.Uri, 34 | UID: service.Uid, 35 | Generation: service.Generation, 36 | CreatedAt: xo.NewTime(service.CreateTime.AsTime()), 37 | } 38 | 39 | if err := s.Insert(ctx, r.db); err != nil { 40 | return fmt.Errorf("failed to save the service: %w", err) 41 | } 42 | 43 | // TODO: should insert by bulk 44 | for k, v := range service.Annotations { 45 | sa := &xo.ServiceAnnotation{ 46 | ServiceParent: parent, 47 | ServiceName: service.Name, 48 | Key: k, 49 | Value: v, 50 | } 51 | 52 | if err := sa.Insert(ctx, r.db); err != nil { 53 | return fmt.Errorf("failed to save the service annotation: %w", err) 54 | } 55 | } 56 | 57 | // TODO: should insert by bulk 58 | for k, v := range service.Labels { 59 | sl := &xo.ServiceLabel{ 60 | ServiceParent: parent, 61 | ServiceName: service.Name, 62 | Key: k, 63 | Value: v, 64 | } 65 | 66 | if err := sl.Insert(ctx, r.db); err != nil { 67 | return fmt.Errorf("failed to save the service label: %w", err) 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func (r *ServicesRepository) CreateServices(ctx context.Context, services []*runpb.Service) error { 75 | // TODO: should insert by bulk 76 | for _, s := range services { 77 | nameParts := strings.Split(s.Name, "/") 78 | if len(nameParts) != 6 { 79 | return fmt.Errorf("invalid service name: %s", s.Name) 80 | } 81 | parent := fmt.Sprintf("projects/%s/locations/%s", nameParts[1], nameParts[3]) 82 | 83 | if err := r.CreateService(ctx, parent, s); err != nil { 84 | return fmt.Errorf("failed to insert service, name=%s : %w", s.Name, err) 85 | } 86 | } 87 | 88 | return nil 89 | } 90 | 91 | // TODO: annotation 92 | func (r *ServicesRepository) ListServices(ctx context.Context, parent string, limit int32) ([]*runpb.Service, error) { 93 | res, err := xo.ListServicesByParentLimit(ctx, r.db, parent, limit) 94 | if err != nil { 95 | return nil, fmt.Errorf("failed to list services: %w", err) 96 | } 97 | 98 | services := make([]*runpb.Service, len(res)) 99 | for i, s := range res { 100 | services[i] = &runpb.Service{ 101 | Name: s.Name, 102 | Description: s.Description, 103 | Uid: s.UID, 104 | Uri: s.URI, 105 | Generation: s.Generation, 106 | CreateTime: timestamppb.New(s.CreatedAt.Time()), 107 | } 108 | 109 | services[i].Annotations = make(map[string]string) 110 | annotations, err := r.listServiceAnnotations(ctx, s.Parent, s.Name) 111 | if err != nil { 112 | return nil, fmt.Errorf("failed to list service annnotations: %w", err) 113 | } 114 | 115 | for _, a := range annotations { 116 | services[i].Annotations[a.Key] = a.Value 117 | } 118 | } 119 | 120 | return services, nil 121 | } 122 | 123 | func (r *ServicesRepository) ListServicesByParentCreatedAtName(ctx context.Context, parent string, createdAt, name string, limit int32) ([]*runpb.Service, error) { 124 | res, err := xo.ListNextServicesByParentCreatedAtNameLimit(ctx, r.db, parent, createdAt, name, limit) 125 | if err != nil { 126 | return nil, fmt.Errorf("failed to list services: %w", err) 127 | } 128 | 129 | services := make([]*runpb.Service, len(res)) 130 | for i, s := range res { 131 | services[i] = &runpb.Service{ 132 | Name: s.Name, 133 | Description: s.Description, 134 | Uid: s.UID, 135 | Uri: s.URI, 136 | Generation: s.Generation, 137 | CreateTime: timestamppb.New(s.CreatedAt.Time()), 138 | } 139 | 140 | services[i].Annotations = make(map[string]string) 141 | annotations, err := r.listServiceAnnotations(ctx, s.Parent, s.Name) 142 | if err != nil { 143 | return nil, fmt.Errorf("failed to list service annnotations: %w", err) 144 | } 145 | 146 | for _, a := range annotations { 147 | services[i].Annotations[a.Key] = a.Value 148 | } 149 | } 150 | 151 | return services, nil 152 | } 153 | 154 | func (s *ServicesRepository) listServiceAnnotations(ctx context.Context, parent, name string) ([]*xo.ServiceAnnotation, error) { 155 | res, err := xo.ListServiceAnnotationsByParentName(ctx, s.db, parent, name) 156 | if err != nil { 157 | return nil, fmt.Errorf("failed to list service annotations: %w", err) 158 | } 159 | 160 | annotations := make([]*xo.ServiceAnnotation, len(res)) 161 | for i, a := range res { 162 | annotations[i] = &xo.ServiceAnnotation{ 163 | ServiceParent: a.ServiceParent, 164 | ServiceName: a.ServiceName, 165 | Key: a.Key, 166 | Value: a.Value, 167 | } 168 | } 169 | 170 | return annotations, nil 171 | } 172 | -------------------------------------------------------------------------------- /internal/handler/db/sqlite/sqlite.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "database/sql" 5 | _ "embed" 6 | "fmt" 7 | 8 | _ "github.com/glebarez/go-sqlite" 9 | ) 10 | 11 | //go:embed schema.sql 12 | var schema []byte 13 | 14 | func NewDB(dataSourceName string) (*sql.DB, error) { 15 | db, err := sql.Open("sqlite", dataSourceName) 16 | if err != nil { 17 | return nil, fmt.Errorf("failed to open the sqlite database: %w", err) 18 | } 19 | 20 | _, err = db.Exec(string(schema)) 21 | if err != nil { 22 | return nil, fmt.Errorf("failed to initialize the sqlite database: %w", err) 23 | } 24 | 25 | return db, nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/handler/db/sqlite/xo/db.xo.go: -------------------------------------------------------------------------------- 1 | // Package xo contains generated code for schema 'cloud-run-api-emulator.db'. 2 | package xo 3 | 4 | // Code generated by xo. DO NOT EDIT. 5 | 6 | import ( 7 | "context" 8 | "database/sql" 9 | "database/sql/driver" 10 | "fmt" 11 | "io" 12 | "time" 13 | ) 14 | 15 | var ( 16 | // logf is used by generated code to log SQL queries. 17 | logf = func(string, ...interface{}) {} 18 | // errf is used by generated code to log SQL errors. 19 | errf = func(string, ...interface{}) {} 20 | ) 21 | 22 | // logerror logs the error and returns it. 23 | func logerror(err error) error { 24 | errf("ERROR: %v", err) 25 | return err 26 | } 27 | 28 | // Logf logs a message using the package logger. 29 | func Logf(s string, v ...interface{}) { 30 | logf(s, v...) 31 | } 32 | 33 | // SetLogger sets the package logger. Valid logger types: 34 | // 35 | // io.Writer 36 | // func(string, ...interface{}) (int, error) // fmt.Printf 37 | // func(string, ...interface{}) // log.Printf 38 | func SetLogger(logger interface{}) { 39 | logf = convLogger(logger) 40 | } 41 | 42 | // Errorf logs an error message using the package error logger. 43 | func Errorf(s string, v ...interface{}) { 44 | errf(s, v...) 45 | } 46 | 47 | // SetErrorLogger sets the package error logger. Valid logger types: 48 | // 49 | // io.Writer 50 | // func(string, ...interface{}) (int, error) // fmt.Printf 51 | // func(string, ...interface{}) // log.Printf 52 | func SetErrorLogger(logger interface{}) { 53 | errf = convLogger(logger) 54 | } 55 | 56 | // convLogger converts logger to the standard logger interface. 57 | func convLogger(logger interface{}) func(string, ...interface{}) { 58 | switch z := logger.(type) { 59 | case io.Writer: 60 | return func(s string, v ...interface{}) { 61 | fmt.Fprintf(z, s, v...) 62 | } 63 | case func(string, ...interface{}) (int, error): // fmt.Printf 64 | return func(s string, v ...interface{}) { 65 | _, _ = z(s, v...) 66 | } 67 | case func(string, ...interface{}): // log.Printf 68 | return z 69 | } 70 | panic(fmt.Sprintf("unsupported logger type %T", logger)) 71 | } 72 | 73 | // DB is the common interface for database operations that can be used with 74 | // types from schema 'cloud-run-api-emulator.db'. 75 | // 76 | // This works with both [database/sql.DB] and [database/sql.Tx]. 77 | type DB interface { 78 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 79 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 80 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 81 | } 82 | 83 | // Error is an error. 84 | type Error string 85 | 86 | // Error satisfies the error interface. 87 | func (err Error) Error() string { 88 | return string(err) 89 | } 90 | 91 | // Error values. 92 | const ( 93 | // ErrAlreadyExists is the already exists error. 94 | ErrAlreadyExists Error = "already exists" 95 | // ErrDoesNotExist is the does not exist error. 96 | ErrDoesNotExist Error = "does not exist" 97 | // ErrMarkedForDeletion is the marked for deletion error. 98 | ErrMarkedForDeletion Error = "marked for deletion" 99 | ) 100 | 101 | // ErrInsertFailed is the insert failed error. 102 | type ErrInsertFailed struct { 103 | Err error 104 | } 105 | 106 | // Error satisfies the error interface. 107 | func (err *ErrInsertFailed) Error() string { 108 | return fmt.Sprintf("insert failed: %v", err.Err) 109 | } 110 | 111 | // Unwrap satisfies the unwrap interface. 112 | func (err *ErrInsertFailed) Unwrap() error { 113 | return err.Err 114 | } 115 | 116 | // ErrUpdateFailed is the update failed error. 117 | type ErrUpdateFailed struct { 118 | Err error 119 | } 120 | 121 | // Error satisfies the error interface. 122 | func (err *ErrUpdateFailed) Error() string { 123 | return fmt.Sprintf("update failed: %v", err.Err) 124 | } 125 | 126 | // Unwrap satisfies the unwrap interface. 127 | func (err *ErrUpdateFailed) Unwrap() error { 128 | return err.Err 129 | } 130 | 131 | // ErrUpsertFailed is the upsert failed error. 132 | type ErrUpsertFailed struct { 133 | Err error 134 | } 135 | 136 | // Error satisfies the error interface. 137 | func (err *ErrUpsertFailed) Error() string { 138 | return fmt.Sprintf("upsert failed: %v", err.Err) 139 | } 140 | 141 | // Unwrap satisfies the unwrap interface. 142 | func (err *ErrUpsertFailed) Unwrap() error { 143 | return err.Err 144 | } 145 | 146 | // ErrInvalidTime is the invalid Time error. 147 | type ErrInvalidTime string 148 | 149 | // Error satisfies the error interface. 150 | func (err ErrInvalidTime) Error() string { 151 | return fmt.Sprintf("invalid Time (%s)", string(err)) 152 | } 153 | 154 | // Time is a SQLite3 Time that scans for the various timestamps values used by 155 | // SQLite3 database drivers to store time.Time values. 156 | type Time struct { 157 | time time.Time 158 | } 159 | 160 | // NewTime creates a time. 161 | func NewTime(t time.Time) Time { 162 | return Time{time: t} 163 | } 164 | 165 | // String satisfies the fmt.Stringer interface. 166 | func (t Time) String() string { 167 | return t.time.String() 168 | } 169 | 170 | // Format formats the time. 171 | func (t Time) Format(layout string) string { 172 | return t.time.Format(layout) 173 | } 174 | 175 | // Time returns a time.Time. 176 | func (t Time) Time() time.Time { 177 | return t.time 178 | } 179 | 180 | // Value satisfies the sql/driver.Valuer interface. 181 | func (t Time) Value() (driver.Value, error) { 182 | return t.time, nil 183 | } 184 | 185 | // Scan satisfies the sql.Scanner interface. 186 | func (t *Time) Scan(v interface{}) error { 187 | switch x := v.(type) { 188 | case time.Time: 189 | t.time = x 190 | return nil 191 | case []byte: 192 | return t.Parse(string(x)) 193 | case string: 194 | return t.Parse(x) 195 | } 196 | return ErrInvalidTime(fmt.Sprintf("%T", v)) 197 | } 198 | 199 | // Parse attempts to Parse string s to t. 200 | func (t *Time) Parse(s string) error { 201 | if s == "" { 202 | return nil 203 | } 204 | for _, f := range TimestampFormats { 205 | if z, err := time.Parse(f, s); err == nil { 206 | t.time = z 207 | return nil 208 | } 209 | } 210 | return ErrInvalidTime(s) 211 | } 212 | 213 | // MarshalJSON satisfies the [json.Marshaler] interface. 214 | func (t Time) MarshalJSON() ([]byte, error) { 215 | return t.time.MarshalJSON() 216 | } 217 | 218 | // UnmarshalJSON satisfies the [json.Unmarshaler] interface. 219 | func (t *Time) UnmarshalJSON(data []byte) error { 220 | return t.time.UnmarshalJSON(data) 221 | } 222 | 223 | // TimestampFormats are the timestamp formats used by SQLite3 database drivers 224 | // to store a time.Time in SQLite3. 225 | // 226 | // The first format in the slice will be used when saving time values into the 227 | // database. When parsing a string from a timestamp or datetime column, the 228 | // formats are tried in order. 229 | var TimestampFormats = []string{ 230 | // By default, use timestamps with the timezone they have. When parsed, 231 | // they will be returned with the same timezone. 232 | "2006-01-02 15:04:05.999999999-07:00", 233 | "2006-01-02T15:04:05.999999999-07:00", 234 | "2006-01-02 15:04:05.999999999", 235 | "2006-01-02T15:04:05.999999999", 236 | "2006-01-02 15:04:05", 237 | "2006-01-02T15:04:05", 238 | "2006-01-02 15:04", 239 | "2006-01-02T15:04", 240 | "2006-01-02", 241 | } 242 | -------------------------------------------------------------------------------- /internal/handler/db/sqlite/xo/listnextservices.xo.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | // Code generated by xo. DO NOT EDIT. 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // ListNextServices represents a row from 'list_next_services'. 10 | type ListNextServices struct { 11 | Parent string `json:"parent"` // parent 12 | Name string `json:"name"` // name 13 | Description string `json:"description"` // description 14 | UID string `json:"uid"` // uid 15 | URI string `json:"uri"` // uri 16 | Generation int64 `json:"generation"` // generation 17 | CreatedAt Time `json:"created_at"` // created_at 18 | } 19 | 20 | // ListNextServicesByParentCreatedAtNameLimit runs a custom query, returning results as [ListNextServices]. 21 | func ListNextServicesByParentCreatedAtNameLimit(ctx context.Context, db DB, parent, created_at, name string, limit int32) ([]*ListNextServices, error) { 22 | // query 23 | const sqlstr = `SELECT` + 24 | ` parent,` + 25 | ` name,` + 26 | ` description,` + 27 | ` uid,` + 28 | ` uri,` + 29 | ` generation,` + 30 | ` created_at` + 31 | ` FROM services` + 32 | ` WHERE` + 33 | ` parent = $1 AND` + 34 | ` created_at <= $2 AND` + 35 | ` (created_at < $2 OR name > $3)` + 36 | ` ORDER BY` + 37 | ` created_at DESC,` + 38 | ` parent ASC,` + 39 | ` name ASC` + 40 | ` LIMIT` + 41 | ` $4;` 42 | // run 43 | logf(sqlstr, parent, created_at, name, limit) 44 | rows, err := db.QueryContext(ctx, sqlstr, parent, created_at, name, limit) 45 | if err != nil { 46 | return nil, logerror(err) 47 | } 48 | defer rows.Close() 49 | // load results 50 | var res []*ListNextServices 51 | for rows.Next() { 52 | var lns ListNextServices 53 | // scan 54 | if err := rows.Scan(&lns.Parent, &lns.Name, &lns.Description, &lns.UID, &lns.URI, &lns.Generation, &lns.CreatedAt); err != nil { 55 | return nil, logerror(err) 56 | } 57 | res = append(res, &lns) 58 | } 59 | if err := rows.Err(); err != nil { 60 | return nil, logerror(err) 61 | } 62 | return res, nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/handler/db/sqlite/xo/listserviceannotations.xo.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | // Code generated by xo. DO NOT EDIT. 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // ListServiceAnnotations represents a row from 'list_service_annotations'. 10 | type ListServiceAnnotations struct { 11 | ServiceParent string `json:"service_parent"` // service_parent 12 | ServiceName string `json:"service_name"` // service_name 13 | Key string `json:"key"` // key 14 | Value string `json:"value"` // value 15 | } 16 | 17 | // ListServiceAnnotationsByParentName runs a custom query, returning results as [ListServiceAnnotations]. 18 | func ListServiceAnnotationsByParentName(ctx context.Context, db DB, parent, name string) ([]*ListServiceAnnotations, error) { 19 | // query 20 | const sqlstr = `SELECT` + 21 | ` service_parent,` + 22 | ` service_name,` + 23 | ` key,` + 24 | ` value` + 25 | ` FROM service_annotations` + 26 | ` WHERE` + 27 | ` service_parent = $1 AND` + 28 | ` service_name = $2` 29 | // run 30 | logf(sqlstr, parent, name) 31 | rows, err := db.QueryContext(ctx, sqlstr, parent, name) 32 | if err != nil { 33 | return nil, logerror(err) 34 | } 35 | defer rows.Close() 36 | // load results 37 | var res []*ListServiceAnnotations 38 | for rows.Next() { 39 | var lsa ListServiceAnnotations 40 | // scan 41 | if err := rows.Scan(&lsa.ServiceParent, &lsa.ServiceName, &lsa.Key, &lsa.Value); err != nil { 42 | return nil, logerror(err) 43 | } 44 | res = append(res, &lsa) 45 | } 46 | if err := rows.Err(); err != nil { 47 | return nil, logerror(err) 48 | } 49 | return res, nil 50 | } 51 | -------------------------------------------------------------------------------- /internal/handler/db/sqlite/xo/listservices.xo.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | // Code generated by xo. DO NOT EDIT. 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // ListServices represents a row from 'list_services'. 10 | type ListServices struct { 11 | Parent string `json:"parent"` // parent 12 | Name string `json:"name"` // name 13 | Description string `json:"description"` // description 14 | UID string `json:"uid"` // uid 15 | URI string `json:"uri"` // uri 16 | Generation int64 `json:"generation"` // generation 17 | CreatedAt Time `json:"created_at"` // created_at 18 | } 19 | 20 | // ListServicesByParentLimit runs a custom query, returning results as [ListServices]. 21 | func ListServicesByParentLimit(ctx context.Context, db DB, parent string, limit int32) ([]*ListServices, error) { 22 | // query 23 | const sqlstr = `SELECT` + 24 | ` parent,` + 25 | ` name,` + 26 | ` description,` + 27 | ` uid,` + 28 | ` uri,` + 29 | ` generation,` + 30 | ` created_at` + 31 | ` FROM services` + 32 | ` WHERE` + 33 | ` parent = $1` + 34 | ` ORDER BY` + 35 | ` created_at DESC,` + 36 | ` parent ASC,` + 37 | ` name ASC` + 38 | ` LIMIT` + 39 | ` $2;` 40 | // run 41 | logf(sqlstr, parent, limit) 42 | rows, err := db.QueryContext(ctx, sqlstr, parent, limit) 43 | if err != nil { 44 | return nil, logerror(err) 45 | } 46 | defer rows.Close() 47 | // load results 48 | var res []*ListServices 49 | for rows.Next() { 50 | var ls ListServices 51 | // scan 52 | if err := rows.Scan(&ls.Parent, &ls.Name, &ls.Description, &ls.UID, &ls.URI, &ls.Generation, &ls.CreatedAt); err != nil { 53 | return nil, logerror(err) 54 | } 55 | res = append(res, &ls) 56 | } 57 | if err := rows.Err(); err != nil { 58 | return nil, logerror(err) 59 | } 60 | return res, nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/handler/db/sqlite/xo/service.xo.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | // Code generated by xo. DO NOT EDIT. 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // Service represents a row from 'services'. 10 | type Service struct { 11 | Parent string `json:"parent"` // parent 12 | Name string `json:"name"` // name 13 | Description string `json:"description"` // description 14 | UID string `json:"uid"` // uid 15 | Generation int64 `json:"generation"` // generation 16 | URI string `json:"uri"` // uri 17 | CreatedAt Time `json:"created_at"` // created_at 18 | // xo fields 19 | _exists, _deleted bool 20 | } 21 | 22 | // Exists returns true when the [Service] exists in the database. 23 | func (s *Service) Exists() bool { 24 | return s._exists 25 | } 26 | 27 | // Deleted returns true when the [Service] has been marked for deletion 28 | // from the database. 29 | func (s *Service) Deleted() bool { 30 | return s._deleted 31 | } 32 | 33 | // Insert inserts the [Service] to the database. 34 | func (s *Service) Insert(ctx context.Context, db DB) error { 35 | switch { 36 | case s._exists: // already exists 37 | return logerror(&ErrInsertFailed{ErrAlreadyExists}) 38 | case s._deleted: // deleted 39 | return logerror(&ErrInsertFailed{ErrMarkedForDeletion}) 40 | } 41 | // insert (manual) 42 | const sqlstr = `INSERT INTO services (` + 43 | `parent, name, description, uid, generation, uri, created_at` + 44 | `) VALUES (` + 45 | `$1, $2, $3, $4, $5, $6, $7` + 46 | `)` 47 | // run 48 | logf(sqlstr, s.Parent, s.Name, s.Description, s.UID, s.Generation, s.URI, s.CreatedAt) 49 | if _, err := db.ExecContext(ctx, sqlstr, s.Parent, s.Name, s.Description, s.UID, s.Generation, s.URI, s.CreatedAt); err != nil { 50 | return logerror(err) 51 | } 52 | // set exists 53 | s._exists = true 54 | return nil 55 | } 56 | 57 | // Update updates a [Service] in the database. 58 | func (s *Service) Update(ctx context.Context, db DB) error { 59 | switch { 60 | case !s._exists: // doesn't exist 61 | return logerror(&ErrUpdateFailed{ErrDoesNotExist}) 62 | case s._deleted: // deleted 63 | return logerror(&ErrUpdateFailed{ErrMarkedForDeletion}) 64 | } 65 | // update with primary key 66 | const sqlstr = `UPDATE services SET ` + 67 | `description = $1, uid = $2, generation = $3, uri = $4, created_at = $5 ` + 68 | `WHERE parent = $6 AND name = $7` 69 | // run 70 | logf(sqlstr, s.Description, s.UID, s.Generation, s.URI, s.CreatedAt, s.Parent, s.Name) 71 | if _, err := db.ExecContext(ctx, sqlstr, s.Description, s.UID, s.Generation, s.URI, s.CreatedAt, s.Parent, s.Name); err != nil { 72 | return logerror(err) 73 | } 74 | return nil 75 | } 76 | 77 | // Save saves the [Service] to the database. 78 | func (s *Service) Save(ctx context.Context, db DB) error { 79 | if s.Exists() { 80 | return s.Update(ctx, db) 81 | } 82 | return s.Insert(ctx, db) 83 | } 84 | 85 | // Upsert performs an upsert for [Service]. 86 | func (s *Service) Upsert(ctx context.Context, db DB) error { 87 | switch { 88 | case s._deleted: // deleted 89 | return logerror(&ErrUpsertFailed{ErrMarkedForDeletion}) 90 | } 91 | // upsert 92 | const sqlstr = `INSERT INTO services (` + 93 | `parent, name, description, uid, generation, uri, created_at` + 94 | `) VALUES (` + 95 | `$1, $2, $3, $4, $5, $6, $7` + 96 | `)` + 97 | ` ON CONFLICT (parent, name) DO ` + 98 | `UPDATE SET ` + 99 | `description = EXCLUDED.description, uid = EXCLUDED.uid, generation = EXCLUDED.generation, uri = EXCLUDED.uri, created_at = EXCLUDED.created_at ` 100 | // run 101 | logf(sqlstr, s.Parent, s.Name, s.Description, s.UID, s.Generation, s.URI, s.CreatedAt) 102 | if _, err := db.ExecContext(ctx, sqlstr, s.Parent, s.Name, s.Description, s.UID, s.Generation, s.URI, s.CreatedAt); err != nil { 103 | return logerror(err) 104 | } 105 | // set exists 106 | s._exists = true 107 | return nil 108 | } 109 | 110 | // Delete deletes the [Service] from the database. 111 | func (s *Service) Delete(ctx context.Context, db DB) error { 112 | switch { 113 | case !s._exists: // doesn't exist 114 | return nil 115 | case s._deleted: // deleted 116 | return nil 117 | } 118 | // delete with composite primary key 119 | const sqlstr = `DELETE FROM services ` + 120 | `WHERE parent = $1 AND name = $2` 121 | // run 122 | logf(sqlstr, s.Parent, s.Name) 123 | if _, err := db.ExecContext(ctx, sqlstr, s.Parent, s.Name); err != nil { 124 | return logerror(err) 125 | } 126 | // set deleted 127 | s._deleted = true 128 | return nil 129 | } 130 | 131 | // ServicesByCreatedAt retrieves a row from 'services' as a [Service]. 132 | // 133 | // Generated from index 'created_at_desc'. 134 | func ServicesByCreatedAt(ctx context.Context, db DB, createdAt Time) ([]*Service, error) { 135 | // query 136 | const sqlstr = `SELECT ` + 137 | `parent, name, description, uid, generation, uri, created_at ` + 138 | `FROM services ` + 139 | `WHERE created_at = $1` 140 | // run 141 | logf(sqlstr, createdAt) 142 | rows, err := db.QueryContext(ctx, sqlstr, createdAt) 143 | if err != nil { 144 | return nil, logerror(err) 145 | } 146 | defer rows.Close() 147 | // process 148 | var res []*Service 149 | for rows.Next() { 150 | s := Service{ 151 | _exists: true, 152 | } 153 | // scan 154 | if err := rows.Scan(&s.Parent, &s.Name, &s.Description, &s.UID, &s.Generation, &s.URI, &s.CreatedAt); err != nil { 155 | return nil, logerror(err) 156 | } 157 | res = append(res, &s) 158 | } 159 | if err := rows.Err(); err != nil { 160 | return nil, logerror(err) 161 | } 162 | return res, nil 163 | } 164 | 165 | // ServiceByParentName retrieves a row from 'services' as a [Service]. 166 | // 167 | // Generated from index 'sqlite_autoindex_services_1'. 168 | func ServiceByParentName(ctx context.Context, db DB, parent, name string) (*Service, error) { 169 | // query 170 | const sqlstr = `SELECT ` + 171 | `parent, name, description, uid, generation, uri, created_at ` + 172 | `FROM services ` + 173 | `WHERE parent = $1 AND name = $2` 174 | // run 175 | logf(sqlstr, parent, name) 176 | s := Service{ 177 | _exists: true, 178 | } 179 | if err := db.QueryRowContext(ctx, sqlstr, parent, name).Scan(&s.Parent, &s.Name, &s.Description, &s.UID, &s.Generation, &s.URI, &s.CreatedAt); err != nil { 180 | return nil, logerror(err) 181 | } 182 | return &s, nil 183 | } 184 | -------------------------------------------------------------------------------- /internal/handler/db/sqlite/xo/serviceannotation.xo.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | // Code generated by xo. DO NOT EDIT. 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // ServiceAnnotation represents a row from 'service_annotations'. 10 | type ServiceAnnotation struct { 11 | ServiceParent string `json:"service_parent"` // service_parent 12 | ServiceName string `json:"service_name"` // service_name 13 | Key string `json:"key"` // key 14 | Value string `json:"value"` // value 15 | // xo fields 16 | _exists, _deleted bool 17 | } 18 | 19 | // Exists returns true when the [ServiceAnnotation] exists in the database. 20 | func (sa *ServiceAnnotation) Exists() bool { 21 | return sa._exists 22 | } 23 | 24 | // Deleted returns true when the [ServiceAnnotation] has been marked for deletion 25 | // from the database. 26 | func (sa *ServiceAnnotation) Deleted() bool { 27 | return sa._deleted 28 | } 29 | 30 | // Insert inserts the [ServiceAnnotation] to the database. 31 | func (sa *ServiceAnnotation) Insert(ctx context.Context, db DB) error { 32 | switch { 33 | case sa._exists: // already exists 34 | return logerror(&ErrInsertFailed{ErrAlreadyExists}) 35 | case sa._deleted: // deleted 36 | return logerror(&ErrInsertFailed{ErrMarkedForDeletion}) 37 | } 38 | // insert (manual) 39 | const sqlstr = `INSERT INTO service_annotations (` + 40 | `service_parent, service_name, key, value` + 41 | `) VALUES (` + 42 | `$1, $2, $3, $4` + 43 | `)` 44 | // run 45 | logf(sqlstr, sa.ServiceParent, sa.ServiceName, sa.Key, sa.Value) 46 | if _, err := db.ExecContext(ctx, sqlstr, sa.ServiceParent, sa.ServiceName, sa.Key, sa.Value); err != nil { 47 | return logerror(err) 48 | } 49 | // set exists 50 | sa._exists = true 51 | return nil 52 | } 53 | 54 | // Update updates a [ServiceAnnotation] in the database. 55 | func (sa *ServiceAnnotation) Update(ctx context.Context, db DB) error { 56 | switch { 57 | case !sa._exists: // doesn't exist 58 | return logerror(&ErrUpdateFailed{ErrDoesNotExist}) 59 | case sa._deleted: // deleted 60 | return logerror(&ErrUpdateFailed{ErrMarkedForDeletion}) 61 | } 62 | // update with primary key 63 | const sqlstr = `UPDATE service_annotations SET ` + 64 | `value = $1 ` + 65 | `WHERE service_parent = $2 AND service_name = $3 AND key = $4` 66 | // run 67 | logf(sqlstr, sa.Value, sa.ServiceParent, sa.ServiceName, sa.Key) 68 | if _, err := db.ExecContext(ctx, sqlstr, sa.Value, sa.ServiceParent, sa.ServiceName, sa.Key); err != nil { 69 | return logerror(err) 70 | } 71 | return nil 72 | } 73 | 74 | // Save saves the [ServiceAnnotation] to the database. 75 | func (sa *ServiceAnnotation) Save(ctx context.Context, db DB) error { 76 | if sa.Exists() { 77 | return sa.Update(ctx, db) 78 | } 79 | return sa.Insert(ctx, db) 80 | } 81 | 82 | // Upsert performs an upsert for [ServiceAnnotation]. 83 | func (sa *ServiceAnnotation) Upsert(ctx context.Context, db DB) error { 84 | switch { 85 | case sa._deleted: // deleted 86 | return logerror(&ErrUpsertFailed{ErrMarkedForDeletion}) 87 | } 88 | // upsert 89 | const sqlstr = `INSERT INTO service_annotations (` + 90 | `service_parent, service_name, key, value` + 91 | `) VALUES (` + 92 | `$1, $2, $3, $4` + 93 | `)` + 94 | ` ON CONFLICT (service_parent, service_name, key) DO ` + 95 | `UPDATE SET ` + 96 | `value = EXCLUDED.value ` 97 | // run 98 | logf(sqlstr, sa.ServiceParent, sa.ServiceName, sa.Key, sa.Value) 99 | if _, err := db.ExecContext(ctx, sqlstr, sa.ServiceParent, sa.ServiceName, sa.Key, sa.Value); err != nil { 100 | return logerror(err) 101 | } 102 | // set exists 103 | sa._exists = true 104 | return nil 105 | } 106 | 107 | // Delete deletes the [ServiceAnnotation] from the database. 108 | func (sa *ServiceAnnotation) Delete(ctx context.Context, db DB) error { 109 | switch { 110 | case !sa._exists: // doesn't exist 111 | return nil 112 | case sa._deleted: // deleted 113 | return nil 114 | } 115 | // delete with composite primary key 116 | const sqlstr = `DELETE FROM service_annotations ` + 117 | `WHERE service_parent = $1 AND service_name = $2 AND key = $3` 118 | // run 119 | logf(sqlstr, sa.ServiceParent, sa.ServiceName, sa.Key) 120 | if _, err := db.ExecContext(ctx, sqlstr, sa.ServiceParent, sa.ServiceName, sa.Key); err != nil { 121 | return logerror(err) 122 | } 123 | // set deleted 124 | sa._deleted = true 125 | return nil 126 | } 127 | 128 | // ServiceAnnotationByServiceParentServiceNameKey retrieves a row from 'service_annotations' as a [ServiceAnnotation]. 129 | // 130 | // Generated from index 'sqlite_autoindex_service_annotations_1'. 131 | func ServiceAnnotationByServiceParentServiceNameKey(ctx context.Context, db DB, serviceParent, serviceName, key string) (*ServiceAnnotation, error) { 132 | // query 133 | const sqlstr = `SELECT ` + 134 | `service_parent, service_name, key, value ` + 135 | `FROM service_annotations ` + 136 | `WHERE service_parent = $1 AND service_name = $2 AND key = $3` 137 | // run 138 | logf(sqlstr, serviceParent, serviceName, key) 139 | sa := ServiceAnnotation{ 140 | _exists: true, 141 | } 142 | if err := db.QueryRowContext(ctx, sqlstr, serviceParent, serviceName, key).Scan(&sa.ServiceParent, &sa.ServiceName, &sa.Key, &sa.Value); err != nil { 143 | return nil, logerror(err) 144 | } 145 | return &sa, nil 146 | } 147 | -------------------------------------------------------------------------------- /internal/handler/db/sqlite/xo/servicelabel.xo.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | // Code generated by xo. DO NOT EDIT. 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // ServiceLabel represents a row from 'service_labels'. 10 | type ServiceLabel struct { 11 | ServiceParent string `json:"service_parent"` // service_parent 12 | ServiceName string `json:"service_name"` // service_name 13 | Key string `json:"key"` // key 14 | Value string `json:"value"` // value 15 | // xo fields 16 | _exists, _deleted bool 17 | } 18 | 19 | // Exists returns true when the [ServiceLabel] exists in the database. 20 | func (sl *ServiceLabel) Exists() bool { 21 | return sl._exists 22 | } 23 | 24 | // Deleted returns true when the [ServiceLabel] has been marked for deletion 25 | // from the database. 26 | func (sl *ServiceLabel) Deleted() bool { 27 | return sl._deleted 28 | } 29 | 30 | // Insert inserts the [ServiceLabel] to the database. 31 | func (sl *ServiceLabel) Insert(ctx context.Context, db DB) error { 32 | switch { 33 | case sl._exists: // already exists 34 | return logerror(&ErrInsertFailed{ErrAlreadyExists}) 35 | case sl._deleted: // deleted 36 | return logerror(&ErrInsertFailed{ErrMarkedForDeletion}) 37 | } 38 | // insert (manual) 39 | const sqlstr = `INSERT INTO service_labels (` + 40 | `service_parent, service_name, key, value` + 41 | `) VALUES (` + 42 | `$1, $2, $3, $4` + 43 | `)` 44 | // run 45 | logf(sqlstr, sl.ServiceParent, sl.ServiceName, sl.Key, sl.Value) 46 | if _, err := db.ExecContext(ctx, sqlstr, sl.ServiceParent, sl.ServiceName, sl.Key, sl.Value); err != nil { 47 | return logerror(err) 48 | } 49 | // set exists 50 | sl._exists = true 51 | return nil 52 | } 53 | 54 | // Update updates a [ServiceLabel] in the database. 55 | func (sl *ServiceLabel) Update(ctx context.Context, db DB) error { 56 | switch { 57 | case !sl._exists: // doesn't exist 58 | return logerror(&ErrUpdateFailed{ErrDoesNotExist}) 59 | case sl._deleted: // deleted 60 | return logerror(&ErrUpdateFailed{ErrMarkedForDeletion}) 61 | } 62 | // update with primary key 63 | const sqlstr = `UPDATE service_labels SET ` + 64 | `value = $1 ` + 65 | `WHERE service_parent = $2 AND service_name = $3 AND key = $4` 66 | // run 67 | logf(sqlstr, sl.Value, sl.ServiceParent, sl.ServiceName, sl.Key) 68 | if _, err := db.ExecContext(ctx, sqlstr, sl.Value, sl.ServiceParent, sl.ServiceName, sl.Key); err != nil { 69 | return logerror(err) 70 | } 71 | return nil 72 | } 73 | 74 | // Save saves the [ServiceLabel] to the database. 75 | func (sl *ServiceLabel) Save(ctx context.Context, db DB) error { 76 | if sl.Exists() { 77 | return sl.Update(ctx, db) 78 | } 79 | return sl.Insert(ctx, db) 80 | } 81 | 82 | // Upsert performs an upsert for [ServiceLabel]. 83 | func (sl *ServiceLabel) Upsert(ctx context.Context, db DB) error { 84 | switch { 85 | case sl._deleted: // deleted 86 | return logerror(&ErrUpsertFailed{ErrMarkedForDeletion}) 87 | } 88 | // upsert 89 | const sqlstr = `INSERT INTO service_labels (` + 90 | `service_parent, service_name, key, value` + 91 | `) VALUES (` + 92 | `$1, $2, $3, $4` + 93 | `)` + 94 | ` ON CONFLICT (service_parent, service_name, key) DO ` + 95 | `UPDATE SET ` + 96 | `value = EXCLUDED.value ` 97 | // run 98 | logf(sqlstr, sl.ServiceParent, sl.ServiceName, sl.Key, sl.Value) 99 | if _, err := db.ExecContext(ctx, sqlstr, sl.ServiceParent, sl.ServiceName, sl.Key, sl.Value); err != nil { 100 | return logerror(err) 101 | } 102 | // set exists 103 | sl._exists = true 104 | return nil 105 | } 106 | 107 | // Delete deletes the [ServiceLabel] from the database. 108 | func (sl *ServiceLabel) Delete(ctx context.Context, db DB) error { 109 | switch { 110 | case !sl._exists: // doesn't exist 111 | return nil 112 | case sl._deleted: // deleted 113 | return nil 114 | } 115 | // delete with composite primary key 116 | const sqlstr = `DELETE FROM service_labels ` + 117 | `WHERE service_parent = $1 AND service_name = $2 AND key = $3` 118 | // run 119 | logf(sqlstr, sl.ServiceParent, sl.ServiceName, sl.Key) 120 | if _, err := db.ExecContext(ctx, sqlstr, sl.ServiceParent, sl.ServiceName, sl.Key); err != nil { 121 | return logerror(err) 122 | } 123 | // set deleted 124 | sl._deleted = true 125 | return nil 126 | } 127 | 128 | // ServiceLabelByServiceParentServiceNameKey retrieves a row from 'service_labels' as a [ServiceLabel]. 129 | // 130 | // Generated from index 'sqlite_autoindex_service_labels_1'. 131 | func ServiceLabelByServiceParentServiceNameKey(ctx context.Context, db DB, serviceParent, serviceName, key string) (*ServiceLabel, error) { 132 | // query 133 | const sqlstr = `SELECT ` + 134 | `service_parent, service_name, key, value ` + 135 | `FROM service_labels ` + 136 | `WHERE service_parent = $1 AND service_name = $2 AND key = $3` 137 | // run 138 | logf(sqlstr, serviceParent, serviceName, key) 139 | sl := ServiceLabel{ 140 | _exists: true, 141 | } 142 | if err := db.QueryRowContext(ctx, sqlstr, serviceParent, serviceName, key).Scan(&sl.ServiceParent, &sl.ServiceName, &sl.Key, &sl.Value); err != nil { 143 | return nil, logerror(err) 144 | } 145 | return &sl, nil 146 | } 147 | -------------------------------------------------------------------------------- /internal/handler/db/yaml/testdata/seed.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | - name: projects/test-project/locations/us-central1/services/service-1 4 | annotations: 5 | annotation-1: value-1 6 | annotation-2: value-2 7 | 8 | - name: projects/test-project/locations/us-central1/services/service-2 9 | labels: 10 | label-1: value-1 11 | label-2: value-2 12 | -------------------------------------------------------------------------------- /internal/handler/db/yaml/yaml.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "cloud.google.com/go/run/apiv2/runpb" 9 | "github.com/goccy/go-yaml" 10 | ) 11 | 12 | type seeds struct { 13 | Services []*runpb.Service `yaml:"services"` 14 | } 15 | 16 | func GetSeeds(path string) (_ []*runpb.Service, reterr error) { 17 | file, err := os.Open(path) 18 | if err != nil { 19 | return nil, fmt.Errorf("failed to open the seed file: %w", err) 20 | } 21 | defer func() { 22 | if err = file.Close(); err != nil { 23 | reterr = fmt.Errorf("failed to close the seed file: %w", err) 24 | } 25 | }() 26 | 27 | content, err := io.ReadAll(file) 28 | if err != nil { 29 | return nil, fmt.Errorf("failed to read the seed file: %w", err) 30 | } 31 | 32 | var s seeds 33 | if err := yaml.Unmarshal(content, &s); err != nil { 34 | return nil, fmt.Errorf("failed to unmarshal the seed file: %w", err) 35 | } 36 | 37 | return s.Services, nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/handler/db/yaml/yaml_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "testing" 5 | 6 | "cloud.google.com/go/run/apiv2/runpb" 7 | "github.com/google/go-cmp/cmp" 8 | "google.golang.org/protobuf/testing/protocmp" 9 | ) 10 | 11 | func TestGetSeeds(t *testing.T) { 12 | t.Parallel() 13 | 14 | want := []*runpb.Service{ 15 | { 16 | Name: "projects/test-project/locations/us-central1/services/service-1", 17 | Annotations: map[string]string{ 18 | "annotation-1": "value-1", 19 | "annotation-2": "value-2", 20 | }, 21 | }, 22 | { 23 | Name: "projects/test-project/locations/us-central1/services/service-2", 24 | Labels: map[string]string{ 25 | "label-1": "value-1", 26 | "label-2": "value-2", 27 | }, 28 | }, 29 | } 30 | 31 | got, err := GetSeeds("./testdata/seed.yaml") 32 | if err != nil { 33 | t.Errorf("failed to get seeds: %v", err) 34 | return 35 | } 36 | 37 | if diff := cmp.Diff(got, want, protocmp.IgnoreFields(&runpb.Service{}, "create_time"), protocmp.Transform()); diff != "" { 38 | t.Errorf("\n(-got, +want)\n%s", diff) 39 | return 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/handler/grpc/executions_server.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import "cloud.google.com/go/run/apiv2/runpb" 4 | 5 | type executionsServer struct { 6 | runpb.UnimplementedExecutionsServer 7 | } 8 | -------------------------------------------------------------------------------- /internal/handler/grpc/grpc.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | 8 | "cloud.google.com/go/run/apiv2/runpb" 9 | "github.com/110y/servergroup" 10 | "google.golang.org/grpc" 11 | 12 | "github.com/kauche/cloud-run-api-emulator/internal/usecase" 13 | ) 14 | 15 | var ( 16 | _ servergroup.Server = (*Server)(nil) 17 | _ servergroup.Stopper = (*Server)(nil) 18 | ) 19 | 20 | type Server struct { 21 | server *grpc.Server 22 | port int 23 | } 24 | 25 | func NewServer(port int, servicesUsecase *usecase.ServicesUsecase) *Server { 26 | server := grpc.NewServer() 27 | 28 | runpb.RegisterServicesServer(server, &servicesServer{uc: servicesUsecase}) 29 | runpb.RegisterRevisionsServer(server, &revisionsServer{}) 30 | runpb.RegisterJobsServer(server, &jobsServer{}) 31 | runpb.RegisterTasksServer(server, &tasksServer{}) 32 | runpb.RegisterExecutionsServer(server, &executionsServer{}) 33 | 34 | return &Server{ 35 | server: server, 36 | port: port, 37 | } 38 | } 39 | 40 | func (s *Server) Start(_ context.Context) error { 41 | lis, err := net.Listen("tcp", fmt.Sprintf(":%d", s.port)) 42 | if err != nil { 43 | return fmt.Errorf("failed to listen on the port %d: %w", s.port, err) 44 | } 45 | 46 | if err := s.server.Serve(lis); err != nil { 47 | return fmt.Errorf("the server aborted: %w", err) 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (s *Server) Stop(_ context.Context) error { 54 | s.server.GracefulStop() 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/handler/grpc/jobs_server.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import "cloud.google.com/go/run/apiv2/runpb" 4 | 5 | type jobsServer struct { 6 | runpb.UnimplementedJobsServer 7 | } 8 | -------------------------------------------------------------------------------- /internal/handler/grpc/revisions_server.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import "cloud.google.com/go/run/apiv2/runpb" 4 | 5 | type revisionsServer struct { 6 | runpb.UnimplementedRevisionsServer 7 | } 8 | -------------------------------------------------------------------------------- /internal/handler/grpc/services_server.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "cloud.google.com/go/longrunning/autogen/longrunningpb" 8 | "cloud.google.com/go/run/apiv2/runpb" 9 | "google.golang.org/grpc/codes" 10 | "google.golang.org/grpc/status" 11 | 12 | "github.com/kauche/cloud-run-api-emulator/internal/usecase" 13 | ) 14 | 15 | type servicesServer struct { 16 | uc *usecase.ServicesUsecase 17 | runpb.UnimplementedServicesServer 18 | } 19 | 20 | func (s *servicesServer) CreateService(ctx context.Context, req *runpb.CreateServiceRequest) (*longrunningpb.Operation, error) { 21 | if err := s.uc.CreateService(ctx, req); err != nil { 22 | // TODO: check if the request is valid 23 | // TODO: check if already exists 24 | 25 | // TODO: log error 26 | return nil, status.Error(codes.Internal, fmt.Sprintf("failed to create the service: %s", err)) 27 | } 28 | 29 | return &longrunningpb.Operation{ 30 | // TODO: fill other fields 31 | Done: true, 32 | }, nil 33 | } 34 | 35 | func (s *servicesServer) ListServices(ctx context.Context, req *runpb.ListServicesRequest) (*runpb.ListServicesResponse, error) { 36 | services, nextPageToken, err := s.uc.ListServices(ctx, req) 37 | if err != nil { 38 | // TODO: log error 39 | return nil, status.Error(codes.Internal, fmt.Sprintf("failed to list services: %s", err)) 40 | } 41 | 42 | return &runpb.ListServicesResponse{ 43 | Services: services, 44 | NextPageToken: nextPageToken, 45 | }, nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/handler/grpc/tasks_server.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import "cloud.google.com/go/run/apiv2/runpb" 4 | 5 | type tasksServer struct { 6 | runpb.UnimplementedTasksServer 7 | } 8 | -------------------------------------------------------------------------------- /internal/usecase/services_usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "cloud.google.com/go/run/apiv2/runpb" 10 | "github.com/kauche/bjt" 11 | "google.golang.org/protobuf/types/known/timestamppb" 12 | 13 | "github.com/kauche/cloud-run-api-emulator/internal/domain" 14 | ) 15 | 16 | var ErrEmptyService = errors.New("service is nil") 17 | 18 | type listServicesPageToken struct { 19 | CreatedAt string 20 | Name string 21 | } 22 | 23 | func NewServicesUsecase(repo domain.ServicesRepository) *ServicesUsecase { 24 | return &ServicesUsecase{ 25 | repo: repo, 26 | } 27 | } 28 | 29 | type ServicesUsecase struct { 30 | repo domain.ServicesRepository 31 | } 32 | 33 | func (u *ServicesUsecase) CreateService(ctx context.Context, req *runpb.CreateServiceRequest) error { 34 | service := req.GetService() 35 | if service == nil { 36 | return ErrEmptyService 37 | } 38 | 39 | service.Name = fmt.Sprintf("%s/services/%s", req.Parent, req.ServiceId) 40 | service.CreateTime = timestamppb.Now() 41 | 42 | if err := u.repo.CreateService(ctx, req.Parent, service); err != nil { 43 | // TODO: check if already exists 44 | return fmt.Errorf("failed to persist the service: %w", err) 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func (u *ServicesUsecase) ListServices(ctx context.Context, req *runpb.ListServicesRequest) ([]*runpb.Service, string, error) { 51 | limit := req.GetPageSize() 52 | if limit == 0 { 53 | limit = 100 // TODO: set the default value that is used in the actual Cloud Run API 54 | } 55 | 56 | // NOTE: Fetch one more service to check if there are more services for the next page 57 | limit++ 58 | 59 | var services []*runpb.Service 60 | 61 | if req.PageToken == "" { 62 | res, err := u.repo.ListServices(ctx, req.Parent, limit) 63 | if err != nil { 64 | return nil, "", fmt.Errorf("failed to list services: %w", err) 65 | } 66 | 67 | services = res 68 | } else { 69 | token, err := bjt.Decode[listServicesPageToken](req.PageToken) 70 | if err != nil { 71 | return nil, "", fmt.Errorf("failed to decode the page token: %w", err) 72 | } 73 | 74 | res, err := u.repo.ListServicesByParentCreatedAtName(ctx, req.Parent, token.Source.CreatedAt, token.Source.Name, limit) 75 | if err != nil { 76 | return nil, "", fmt.Errorf("failed to list services by parent, created_at and name: %w", err) 77 | } 78 | 79 | services = res 80 | } 81 | 82 | numFetchedServices := len(services) 83 | 84 | if numFetchedServices == 0 { 85 | return []*runpb.Service{}, "", nil 86 | } else if int32(numFetchedServices) < limit { 87 | return services, "", nil 88 | } 89 | 90 | lastService := services[numFetchedServices-2] 91 | 92 | // NOTE: Service.Name consists of projects/{project}/locations/{location}/services/{service_id} 93 | nameParts := strings.Split(lastService.Name, "/") 94 | if len(nameParts) != 6 { 95 | return nil, "", fmt.Errorf("invalid service name: %s", lastService.Name) 96 | } 97 | lastServiceName := nameParts[5] 98 | 99 | pageToken := bjt.NewToken(&listServicesPageToken{ 100 | CreatedAt: lastService.CreateTime.AsTime().Format("2006-01-02 15:04:05.999999999"), 101 | Name: lastServiceName, 102 | }) 103 | 104 | token, err := pageToken.Encode() 105 | if err != nil { 106 | return nil, "", fmt.Errorf("failed to encode the page token: %w", err) 107 | } 108 | 109 | return services[:numFetchedServices-1], token, nil 110 | } 111 | -------------------------------------------------------------------------------- /tools/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kauche/cloud-run-api-emulator/tools 2 | 3 | go 1.20 4 | 5 | require github.com/xo/xo v0.0.0-20230510002142-32310f769e8e 6 | 7 | require ( 8 | github.com/Masterminds/goutils v1.1.1 // indirect 9 | github.com/Masterminds/semver/v3 v3.2.1 // indirect 10 | github.com/Masterminds/sprig/v3 v3.2.3 // indirect 11 | github.com/fatih/color v1.15.0 // indirect 12 | github.com/go-sql-driver/mysql v1.7.1 // indirect 13 | github.com/gobwas/glob v0.2.3 // indirect 14 | github.com/goccy/go-yaml v1.11.0 // indirect 15 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect 16 | github.com/golang-sql/sqlexp v0.1.0 // indirect 17 | github.com/google/go-cmp v0.5.9 // indirect 18 | github.com/google/uuid v1.3.0 // indirect 19 | github.com/huandu/xstrings v1.4.0 // indirect 20 | github.com/imdario/mergo v0.3.15 // indirect 21 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 22 | github.com/kenshaw/inflector v0.2.0 // indirect 23 | github.com/kenshaw/snaker v0.2.0 // indirect 24 | github.com/lib/pq v1.10.9 // indirect 25 | github.com/mattn/go-colorable v0.1.13 // indirect 26 | github.com/mattn/go-isatty v0.0.18 // indirect 27 | github.com/mattn/go-sqlite3 v1.14.16 // indirect 28 | github.com/microsoft/go-mssqldb v0.21.0 // indirect 29 | github.com/mitchellh/copystructure v1.2.0 // indirect 30 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 31 | github.com/shopspring/decimal v1.3.1 // indirect 32 | github.com/sijms/go-ora/v2 v2.7.4 // indirect 33 | github.com/spf13/cast v1.5.0 // indirect 34 | github.com/spf13/cobra v1.7.0 // indirect 35 | github.com/spf13/pflag v1.0.5 // indirect 36 | github.com/traefik/yaegi v0.15.1 // indirect 37 | github.com/xo/dburl v0.14.2 // indirect 38 | github.com/yookoala/realpath v1.0.0 // indirect 39 | golang.org/x/crypto v0.9.0 // indirect 40 | golang.org/x/mod v0.10.0 // indirect 41 | golang.org/x/sys v0.8.0 // indirect 42 | golang.org/x/tools v0.9.1 // indirect 43 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 44 | mvdan.cc/gofumpt v0.5.0 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /tools/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= 2 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= 3 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.1/go.mod h1:gLa1CL2RNE4s7M3yopJ/p0iq5DdY6Yv5ZUt9MTRZOQM= 4 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= 5 | github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1/go.mod h1:4qFor3D/HDsvBME35Xy9rwW9DecL+M2sNw1ybjPtwA0= 6 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 7 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 8 | github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= 9 | github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= 10 | github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= 11 | github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= 12 | github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= 13 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= 18 | github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= 19 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 20 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 21 | github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= 22 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 23 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 24 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 25 | github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= 26 | github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 27 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 28 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 29 | github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= 30 | github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= 31 | github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 32 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 33 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 34 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 35 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= 36 | github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= 37 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 38 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 39 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 40 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 41 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 42 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 43 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 44 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 45 | github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 46 | github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= 47 | github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 48 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 49 | github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= 50 | github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 51 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 52 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 53 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 54 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 55 | github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= 56 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 57 | github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= 58 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 59 | github.com/kenshaw/inflector v0.2.0 h1:6HuXXlzqIIptlIkKvZ4fFSgfr0opnV6/LVIg+1+DlqY= 60 | github.com/kenshaw/inflector v0.2.0/go.mod h1:g5nxVgwZsIPE0eesk201Sp4YBwDDHZDfJHl6L2PUTM4= 61 | github.com/kenshaw/snaker v0.2.0 h1:DPlxCtAv9mw1wSsvIN1khUAPJUIbFJUckMIDWSQ7TC8= 62 | github.com/kenshaw/snaker v0.2.0/go.mod h1:DNyRUqHMZ18/zioxr6R7m4kSxxf2+QmB0BXoORsXRaY= 63 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 64 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 65 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 66 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 67 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 68 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 69 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 70 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 71 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 72 | github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= 73 | github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 74 | github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= 75 | github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 76 | github.com/microsoft/go-mssqldb v0.21.0 h1:p2rpHIL7TlSv1QrbXJUAcbyRKnIT0C9rRkH2E4OjLn8= 77 | github.com/microsoft/go-mssqldb v0.21.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4= 78 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 79 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 80 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 81 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 82 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 83 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 84 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= 85 | github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= 86 | github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= 87 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= 88 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 89 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 90 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 91 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 92 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 93 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 94 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 95 | github.com/sijms/go-ora/v2 v2.7.4 h1:wBIpWMgossglXmtO5Y0AUQfD3Q0aWzYS5H1fLTgppRk= 96 | github.com/sijms/go-ora/v2 v2.7.4/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk= 97 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 98 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= 99 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 100 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 101 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 102 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 103 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 104 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 105 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 106 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 107 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 108 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 109 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 110 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 111 | github.com/traefik/yaegi v0.15.1 h1:YA5SbaL6HZA0Exh9T/oArRHqGN2HQ+zgmCY7dkoTXu4= 112 | github.com/traefik/yaegi v0.15.1/go.mod h1:AVRxhaI2G+nUsaM1zyktzwXn69G3t/AuTDrCiTds9p0= 113 | github.com/xo/dburl v0.14.2 h1:tqiXv1glyxFph3LA39RXE4TYidr/yp7kG2YDrgJVjiA= 114 | github.com/xo/dburl v0.14.2/go.mod h1:B7/G9FGungw6ighV8xJNwWYQPMfn3gsi2sn5SE8Bzco= 115 | github.com/xo/xo v0.0.0-20230510002142-32310f769e8e h1:ISuNgXGv0Ipm8p5AqrR90yCi5SjQn5o/0JKmcXEfzgs= 116 | github.com/xo/xo v0.0.0-20230510002142-32310f769e8e/go.mod h1:Djhneq4HVQ2Q6mu4mCs3+FBITwKe7WRECGTuyoRgpxo= 117 | github.com/yookoala/realpath v1.0.0 h1:7OA9pj4FZd+oZDsyvXWQvjn5oBdcHRTV44PpdMSuImQ= 118 | github.com/yookoala/realpath v1.0.0/go.mod h1:gJJMA9wuX7AcqLy1+ffPatSCySA1FQ2S8Ya9AIoYBpE= 119 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 120 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 121 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 122 | golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 123 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 124 | golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 125 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 126 | golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 127 | golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= 128 | golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= 129 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 130 | golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= 131 | golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 132 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 133 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 134 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 135 | golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 136 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 137 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 138 | golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 139 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 140 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 141 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 142 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 143 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 144 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 148 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 149 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 150 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 151 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 152 | golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 153 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 154 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 155 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 156 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 157 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 158 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 159 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 160 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 161 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 162 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 163 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 164 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 165 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 166 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 167 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 168 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 169 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 170 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 171 | golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= 172 | golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= 173 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 174 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= 175 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 176 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 177 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= 178 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= 179 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 180 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 181 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 182 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 183 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 184 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 185 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 186 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 187 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 188 | mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= 189 | mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= 190 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import _ "github.com/xo/xo" 4 | --------------------------------------------------------------------------------