Skip to content
This repository was archived by the owner on Jun 22, 2023. It is now read-only.

Commit 4bb1285

Browse files
Merge pull request #9 from varshaprasad96/revert/kb-scaffolding
Revert KB default scaffolding
2 parents 381028f + e1b9a89 commit 4bb1285

35 files changed

Lines changed: 885 additions & 371 deletions

Dockerfile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Build the manager binary
2-
FROM golang:1.18 as builder
2+
FROM golang:1.19 as builder
3+
ARG TARGETOS
4+
ARG TARGETARCH
35

46
WORKDIR /workspace
57
# Copy the Go Modules manifests
@@ -15,7 +17,11 @@ COPY api/ api/
1517
COPY controllers/ controllers/
1618

1719
# Build
18-
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go
20+
# the GOARCH has not a default value to allow the binary be built according to the host where the command
21+
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
22+
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
23+
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
24+
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager main.go
1925

2026
# Use distroless as minimal base image to package the manager binary
2127
# Refer to https://github.com/GoogleContainerTools/distroless for more details

Makefile

Lines changed: 122 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11

22
# Image URL to use all building/pushing image targets
33
IMG ?= controller:latest
4+
REGISTRY ?= localhost
45
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
5-
ENVTEST_K8S_VERSION = 1.24
6+
ENVTEST_K8S_VERSION = 1.25.0
7+
OS ?= $(shell go env GOOS )
8+
ARCH ?= $(shell go env GOARCH )
69

710
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
811
ifeq (,$(shell go env GOBIN))
@@ -12,14 +15,16 @@ GOBIN=$(shell go env GOBIN)
1215
endif
1316

1417
# Setting SHELL to bash allows bash commands to be executed by recipes.
15-
# This is a requirement for 'setup-envtest.sh' in the test target.
1618
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
1719
SHELL = /usr/bin/env bash -o pipefail
1820
.SHELLFLAGS = -ec
1921

2022
.PHONY: all
2123
all: build
2224

25+
# kcp specific
26+
APIEXPORT_PREFIX ?= catalog
27+
2328
##@ General
2429

2530
# The help target prints out all targets with their descriptions organized
@@ -41,12 +46,16 @@ help: ## Display this help.
4146

4247
.PHONY: manifests
4348
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
44-
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd
49+
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
4550

4651
.PHONY: generate
4752
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
4853
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
4954

55+
.PHONY: apiresourceschemas
56+
apiresourceschemas: $(KUSTOMIZE) ## Convert CRDs from config/crds to APIResourceSchemas. Specify APIEXPORT_PREFIX as needed.
57+
$(KUSTOMIZE) build config/crd | kubectl kcp crd snapshot -f - --prefix $(APIEXPORT_PREFIX) > config/kcp/$(APIEXPORT_PREFIX).apiresourceschemas.yaml
58+
5059
.PHONY: fmt
5160
fmt: ## Run go fmt against code.
5261
go fmt ./...
@@ -57,18 +66,77 @@ vet: ## Run go vet against code.
5766

5867
.PHONY: test
5968
test: manifests generate fmt vet envtest ## Run tests.
60-
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out
69+
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out
70+
71+
ARTIFACT_DIR ?= .test
72+
73+
.PHONY: test-e2e
74+
test-e2e: $(ARTIFACT_DIR)/kind.kubeconfig kcp-synctarget run-test-e2e## Set up prerequisites and run end-to-end tests on a cluster.
75+
76+
.PHONY: run-test-e2e
77+
run-test-e2e: ## Run end-to-end tests on a cluster.
78+
go test ./test/e2e/... --kubeconfig $(abspath $(ARTIFACT_DIR)/kcp.kubeconfig) --workspace $(shell $(KCP_KUBECTL) kcp workspace . --short)
79+
80+
.PHONY: kind-image
81+
kind-image: docker-build ## Load the controller-manager image into the kind cluster.
82+
kind load docker-image $(REGISTRY)/$(IMG) --name catalog
83+
84+
$(ARTIFACT_DIR)/kind.kubeconfig: $(ARTIFACT_DIR) ## Run a kind cluster and generate a $KUBECONFIG for it.
85+
@if ! kind get clusters --quiet | grep --quiet catalog; then kind create cluster --name catalog; fi
86+
kind get kubeconfig --name catalog > $(ARTIFACT_DIR)/kind.kubeconfig
87+
88+
$(ARTIFACT_DIR): ## Create a directory for test artifacts.
89+
mkdir -p $(ARTIFACT_DIR)
90+
91+
KCP_KUBECTL ?= PATH=$(LOCALBIN):$(PATH) KUBECONFIG=$(ARTIFACT_DIR)/kcp.kubeconfig kubectl
92+
KIND_KUBECTL ?= kubectl --kubeconfig $(ARTIFACT_DIR)/kind.kubeconfig
93+
94+
.PHONY: kcp-synctarget
95+
kcp-synctarget: kcp-workspace $(ARTIFACT_DIR)/syncer.yaml $(YQ) ## Add the kind cluster to kcp as a target for workloads.
96+
$(KIND_KUBECTL) apply -f $(ARTIFACT_DIR)/syncer.yaml
97+
$(eval DEPLOYMENT_NAME = $(shell $(YQ) 'select(.kind=="Deployment") | .metadata.name' < $(ARTIFACT_DIR)/syncer.yaml ))
98+
$(eval DEPLOYMENT_NAMESPACE = $(shell $(YQ) 'select(.kind=="Deployment") | .metadata.namespace' < $(ARTIFACT_DIR)/syncer.yaml ))
99+
$(KIND_KUBECTL) --namespace $(DEPLOYMENT_NAMESPACE) rollout status deployment/$(DEPLOYMENT_NAME)
100+
@if [[ ! -s $(ARTIFACT_DIR)/syncer.log ]]; then ( $(KIND_KUBECTL) --namespace $(DEPLOYMENT_NAMESPACE) logs deployment/$(DEPLOYMENT_NAME) -f >$(ARTIFACT_DIR)/syncer.log 2>&1 & ); fi
101+
$(KCP_KUBECTL) wait --for=condition=Ready synctarget/catalog
102+
103+
$(ARTIFACT_DIR)/syncer.yaml: ## Generate the manifests necessary to register the kind cluster with kcp.
104+
$(KCP_KUBECTL) kcp workload sync catalog --resources services --syncer-image ghcr.io/kcp-dev/kcp/syncer:v$(KCP_VERSION) --output-file $(ARTIFACT_DIR)/syncer.yaml
105+
106+
.PHONY: kcp-workspace
107+
kcp-workspace: $(KUBECTL_KCP) kcp-server ## Create a workspace in kcp for the controller-manager.
108+
$(KCP_KUBECTL) kcp workspace use '~'
109+
@if ! $(KCP_KUBECTL) kcp workspace use catalog; then $(KCP_KUBECTL) kcp workspace create catalog --type universal --enter; fi
110+
111+
.PHONY: kcp-server
112+
kcp-server: $(KCP) $(ARTIFACT_DIR)/kcp ## Run the kcp server.
113+
@if [[ ! -s $(ARTIFACT_DIR)/kcp.log ]]; then ( $(KCP) start -v 5 --root-directory $(ARTIFACT_DIR)/kcp --kubeconfig-path $(ARTIFACT_DIR)/kcp.kubeconfig --audit-log-maxsize 1024 --audit-log-mode=batch --audit-log-batch-max-wait=1s --audit-log-batch-max-size=1000 --audit-log-batch-buffer-size=10000 --audit-log-batch-throttle-burst=15 --audit-log-batch-throttle-enable=true --audit-log-batch-throttle-qps=10 --audit-policy-file ./test/e2e/audit-policy.yaml --audit-log-path $(ARTIFACT_DIR)/audit.log >$(ARTIFACT_DIR)/kcp.log 2>&1 & ); fi
114+
@while true; do if [[ ! -s $(ARTIFACT_DIR)/kcp.kubeconfig ]]; then sleep 0.2; else break; fi; done
115+
@while true; do if ! kubectl --kubeconfig $(ARTIFACT_DIR)/kcp.kubeconfig get --raw /readyz >$(ARTIFACT_DIR)/kcp.probe.log 2>&1; then sleep 0.2; else break; fi; done
116+
117+
$(ARTIFACT_DIR)/kcp: ## Create a directory for the kcp server data.
118+
mkdir -p $(ARTIFACT_DIR)/kcp
119+
120+
.PHONY: test-e2e-cleanup
121+
test-e2e-cleanup: ## Clean up processes and directories from an end-to-end test run.
122+
kind delete cluster --name catalog || true
123+
rm -rf $(ARTIFACT_DIR) || true
124+
pkill -sigterm kcp || true
125+
pkill -sigterm kubectl || true
61126

62127
##@ Build
63128

64129
.PHONY: build
65-
build: generate fmt vet ## Build manager binary.
130+
build: manifests generate fmt vet ## Build manager binary.
66131
go build -o bin/manager main.go
67132

68133
.PHONY: run
69134
run: manifests generate fmt vet ## Run a controller from your host.
70135
go run ./main.go
71136

137+
# If you wish built the manager image targeting other platforms you can use the --platform flag.
138+
# (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it.
139+
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
72140
.PHONY: docker-build
73141
docker-build: test ## Build docker image with the manager.
74142
docker build -t ${IMG} .
@@ -77,12 +145,31 @@ docker-build: test ## Build docker image with the manager.
77145
docker-push: ## Push docker image with the manager.
78146
docker push ${IMG}
79147

148+
# PLATFORMS defines the target platforms for the manager image be build to provide support to multiple
149+
# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
150+
# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/
151+
# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/
152+
# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=<myregistry/image:<tag>> than the export will fail)
153+
# To properly provided solutions that supports more than one platform you should use this option.
154+
PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
155+
.PHONY: docker-buildx
156+
docker-buildx: test ## Build and push docker image for the manager for cross-platform support
157+
# copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
158+
sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
159+
- docker buildx create --name project-v3-builder
160+
docker buildx use project-v3-builder
161+
- docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
162+
- docker buildx rm project-v3-builder
163+
rm Dockerfile.cross
164+
80165
##@ Deployment
81166

82167
ifndef ignore-not-found
83168
ignore-not-found = false
84169
endif
85170

171+
KUBECONFIG ?= $(abspath ~/.kube/config )
172+
86173
.PHONY: install
87174
install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
88175
$(KUSTOMIZE) build config/crd | kubectl apply -f -
@@ -91,6 +178,11 @@ install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~
91178
uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
92179
$(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
93180

181+
.PHONY: deploy-crd
182+
deploy-crd: manifests $(KUSTOMIZE) ## Deploy controller
183+
cd config/manager && $(KUSTOMIZE) edit set image controller=${REGISTRY}/${IMG}
184+
$(KUSTOMIZE) build config/default-crd | kubectl --kubeconfig $(KUBECONFIG) apply -f - || true
185+
94186
.PHONY: deploy
95187
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
96188
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
@@ -111,23 +203,44 @@ $(LOCALBIN):
111203
KUSTOMIZE ?= $(LOCALBIN)/kustomize
112204
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
113205
ENVTEST ?= $(LOCALBIN)/setup-envtest
206+
KCP ?= $(LOCALBIN)/kcp
207+
KUBECTL_KCP ?= $(LOCALBIN)/kubectl-kcp
208+
YQ ?= $(LOCALBIN)/yq
114209

115210
## Tool Versions
116211
KUSTOMIZE_VERSION ?= v3.8.7
117-
CONTROLLER_TOOLS_VERSION ?= v0.8.0
212+
CONTROLLER_TOOLS_VERSION ?= v0.10.0
213+
KCP_VERSION ?= 0.9.1
214+
YQ_VERSION ?= v4.27.2
118215

119216
KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"
120217
.PHONY: kustomize
121218
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
122219
$(KUSTOMIZE): $(LOCALBIN)
123-
curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN)
220+
test -s $(LOCALBIN)/kustomize || { curl -Ss $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); }
124221

125222
.PHONY: controller-gen
126223
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
127224
$(CONTROLLER_GEN): $(LOCALBIN)
128-
GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
225+
test -s $(LOCALBIN)/controller-gen || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
129226

130227
.PHONY: envtest
131228
envtest: $(ENVTEST) ## Download envtest-setup locally if necessary.
132229
$(ENVTEST): $(LOCALBIN)
133-
GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
230+
test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
231+
232+
$(YQ): ## Download yq locally if necessary.
233+
mkdir -p $(LOCALBIN)
234+
GOBIN=$(LOCALBIN) go install github.com/mikefarah/yq/v4@$(YQ_VERSION)
235+
236+
.PHONY: kcp
237+
$(KCP): ## Download kcp locally if necessary.
238+
mkdir -p $(LOCALBIN)
239+
curl -L -s -o - https://github.com/kcp-dev/kcp/releases/download/v$(KCP_VERSION)/kcp_$(KCP_VERSION)_$(OS)_$(ARCH).tar.gz | tar --directory $(LOCALBIN)/../ -xvzf - bin/kcp
240+
touch $(KCP) # we download an "old" file, so make will re-download to refresh it unless we make it newer than the owning dir
241+
242+
$(KUBECTL_KCP): ## Download kcp kubectl plugins locally if necessary.
243+
mkdir -p $(LOCALBIN)
244+
curl -L -s -o - https://github.com/kcp-dev/kcp/releases/download/v$(KCP_VERSION)/kubectl-kcp-plugin_$(KCP_VERSION)_$(OS)_$(ARCH).tar.gz | tar --directory $(LOCALBIN)/../ -xvzf - bin
245+
touch $(KUBECTL_KCP) # we download an "old" file, so make will re-download to refresh it unless we make it newer than the owning dir
246+

PROJECT

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
domain: kcp.dev
2+
layout:
3+
- go.kubebuilder.io/v3
4+
projectName: catalog
5+
repo: github.com/kcp-dev/catalog
6+
resources:
7+
- api:
8+
crdVersion: v1
9+
namespaced: true
10+
controller: true
11+
domain: kcp.dev
12+
group: catalog
13+
kind: CatalogEntry
14+
path: github.com/kcp-dev/catalog/api/v1alpha1
15+
version: v1alpha1
16+
version: "3"

api/v1alpha1/groupversion_info.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
// Package v1alpha1 contains API Schema definitions for the catalog.kcp.dev v1alpha1 API group
18-
//+kubebuilder:object:generate=true
19-
//+groupName=catalog.kcp.dev
17+
// Package v1alpha1 contains API Schema definitions for the catalog v1alpha1 API group
18+
// +kubebuilder:object:generate=true
19+
// +groupName=catalog.kcp.dev
2020
package v1alpha1
2121

2222
import (

config/crd/catalog.kcp.dev_catalogentries.yaml renamed to config/crd/bases/catalog.kcp.dev_catalogentries.yaml

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.8.0
6+
controller-gen.kubebuilder.io/version: v0.10.0
77
creationTimestamp: null
88
name: catalogentries.catalog.kcp.dev
99
spec:
@@ -57,9 +57,8 @@ spec:
5757
type: string
5858
path:
5959
description: path is an absolute reference to a workspace,
60-
e.g. root:org:ws. The workspace must be some ancestor
61-
or a child of some ancestor. If it is unset, the path
62-
of the APIBinding is used.
60+
e.g. root:org:ws. If it is unset, the path of the APIBinding
61+
is used.
6362
pattern: ^root(:[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
6463
type: string
6564
required:
@@ -125,11 +124,12 @@ spec:
125124
by the API provider(s) for this catalog entry.
126125
items:
127126
description: PermissionClaim identifies an object by GR and identity
128-
hash. It's purpose is to determine the added permisions that a
127+
hash. Its purpose is to determine the added permissions that a
129128
service provider may request and that a consumer may accept and
130-
alllow the service provider access to.
129+
allow the service provider access to.
131130
properties:
132131
group:
132+
default: ""
133133
description: group is the name of an API group. For core groups
134134
this is the empty string '""'.
135135
pattern: ^(|[a-z0-9]([-a-z0-9]*[a-z0-9](\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)?)$
@@ -174,9 +174,3 @@ spec:
174174
storage: true
175175
subresources:
176176
status: {}
177-
status:
178-
acceptedNames:
179-
kind: ""
180-
plural: ""
181-
conditions: []
182-
storedVersions: []

config/crd/kustomization.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# This kustomization.yaml is not intended to be run by itself,
2+
# since it depends on service name and namespace that are out of this kustomize package.
3+
# It should be run by config/default
4+
resources:
5+
- bases/catalog.kcp.dev_catalogentries.yaml
6+
#+kubebuilder:scaffold:crdkustomizeresource
7+
8+
patchesStrategicMerge:
9+
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
10+
# patches here are for enabling the conversion webhook for each CRD
11+
#- patches/webhook_in_catalogentries.yaml
12+
#+kubebuilder:scaffold:crdkustomizewebhookpatch
13+
14+
# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
15+
# patches here are for enabling the CA injection for each CRD
16+
#- patches/cainjection_in_catalogentries.yaml
17+
#+kubebuilder:scaffold:crdkustomizecainjectionpatch
18+
19+
# the following config is for teaching kustomize how to do kustomization for CRDs.
20+
configurations:
21+
- kustomizeconfig.yaml

config/crd/kustomizeconfig.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# This file is for teaching kustomize how to substitute name and namespace reference in CRD
2+
nameReference:
3+
- kind: Service
4+
version: v1
5+
fieldSpecs:
6+
- kind: CustomResourceDefinition
7+
version: v1
8+
group: apiextensions.k8s.io
9+
path: spec/conversion/webhook/clientConfig/service/name
10+
11+
namespace:
12+
- kind: CustomResourceDefinition
13+
version: v1
14+
group: apiextensions.k8s.io
15+
path: spec/conversion/webhook/clientConfig/service/namespace
16+
create: false
17+
18+
varReference:
19+
- path: metadata/annotations
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# The following patch adds a directive for certmanager to inject CA into the CRD
2+
apiVersion: apiextensions.k8s.io/v1
3+
kind: CustomResourceDefinition
4+
metadata:
5+
annotations:
6+
cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
7+
name: catalogentries.catalog.kcp.dev
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# The following patch enables a conversion webhook for the CRD
2+
apiVersion: apiextensions.k8s.io/v1
3+
kind: CustomResourceDefinition
4+
metadata:
5+
name: catalogentries.catalog.kcp.dev
6+
spec:
7+
conversion:
8+
strategy: Webhook
9+
webhook:
10+
clientConfig:
11+
service:
12+
namespace: system
13+
name: webhook-service
14+
path: /convert
15+
conversionReviewVersions:
16+
- v1

0 commit comments

Comments
 (0)