Skip to content

Commit c5710f6

Browse files
committed
wip: add kube-api-linter, fix linter issues
Signed-off-by: Bryce Palmer <bpalmer@redhat.com>
1 parent a449fc4 commit c5710f6

File tree

7 files changed

+77
-32
lines changed

7 files changed

+77
-32
lines changed

.custom-gcl.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
version: v1.64.8
2+
name: golangci-kube-api-linter
3+
destination: ./bin
4+
plugins:
5+
- module: 'sigs.k8s.io/kube-api-linter'
6+
version: 'v0.0.0-20250626111229-e719da12d840' # Replace with the latest version

.golangci.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ linters:
3737
- unparam
3838
- unused
3939
- whitespace
40+
- kubeapilinter
4041

4142
linters-settings:
4243
gci:
@@ -69,7 +70,27 @@ linters-settings:
6970
alias: bsemver
7071
- pkg: "^github.com/operator-framework/operator-controller/internal/util/([^/]+)$"
7172
alias: "${1}util"
73+
74+
custom:
75+
kubeapilinter:
76+
type: "module"
77+
description: Kube API Linter lints Kube like APIs based on API conventions and best practices.
78+
settings:
79+
linters: {}
80+
lintersConfig:
81+
optionalFields:
82+
pointers:
83+
preference: WhenRequired
84+
policy: Warn
85+
omitempty:
86+
policy: Warn
7287

7388
output:
7489
formats:
7590
- format: tab
91+
92+
issues:
93+
exclude-rules:
94+
- path-except: "api/*"
95+
linters:
96+
- kubeapilinter

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ help-extended: #HELP Display extended help.
113113

114114
.PHONY: lint
115115
lint: lint-custom $(GOLANGCI_LINT) #HELP Run golangci linter.
116-
$(GOLANGCI_LINT) run --build-tags $(GO_BUILD_TAGS) $(GOLANGCI_LINT_ARGS)
116+
$(GOLANGCI_LINT) custom
117+
./bin/golangci-kube-api-linter run --build-tags $(GO_BUILD_TAGS) $(GOLANGCI_LINT_ARGS)
117118

118119
.PHONY: custom-linter-build
119120
custom-linter-build: #EXHELP Build custom linter

api/v1/clustercatalog_types.go

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,25 +54,27 @@ const (
5454
// ClusterCatalog enables users to make File-Based Catalog (FBC) catalog data available to the cluster.
5555
// For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs
5656
type ClusterCatalog struct {
57+
// +optional
5758
metav1.TypeMeta `json:",inline"`
5859

5960
// metadata is the standard object's metadata.
6061
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
62+
// +optional
6163
metav1.ObjectMeta `json:"metadata"`
6264

6365
// spec is the desired state of the ClusterCatalog.
6466
// spec is required.
6567
// The controller will work to ensure that the desired
6668
// catalog is unpacked and served over the catalog content HTTP server.
67-
// +kubebuilder:validation:Required
69+
// +required
6870
Spec ClusterCatalogSpec `json:"spec"`
6971

7072
// status contains information about the state of the ClusterCatalog such as:
7173
// - Whether or not the catalog contents are being served via the catalog content HTTP server
7274
// - Whether or not the ClusterCatalog is progressing to a new state
7375
// - A reference to the source from which the catalog contents were retrieved
7476
// +optional
75-
Status ClusterCatalogStatus `json:"status,omitempty"`
77+
Status ClusterCatalogStatus `json:"status,omitempty"` //nolint: kubeapilinter
7678
}
7779

7880
//+kubebuilder:object:root=true
@@ -87,7 +89,7 @@ type ClusterCatalogList struct {
8789

8890
// items is a list of ClusterCatalogs.
8991
// items is required.
90-
// +kubebuilder:validation:Required
92+
// +required
9193
Items []ClusterCatalog `json:"items"`
9294
}
9395

@@ -110,7 +112,7 @@ type ClusterCatalogSpec struct {
110112
// image:
111113
// ref: quay.io/operatorhubio/catalog:latest
112114
//
113-
// +kubebuilder:validation:Required
115+
// +required
114116
Source CatalogSource `json:"source"`
115117

116118
// priority allows the user to define a priority for a ClusterCatalog.
@@ -131,8 +133,8 @@ type ClusterCatalogSpec struct {
131133
// The highest possible value is 2147483647.
132134
//
133135
// +kubebuilder:default:=0
134-
// +kubebuilder:validation:minimum:=-2147483648
135-
// +kubebuilder:validation:maximum:=2147483647
136+
// +kubebuilder:validation:Minimum:=-2147483648
137+
// +kubebuilder:validation:Maximum:=2147483647
136138
// +optional
137139
Priority int32 `json:"priority"`
138140

@@ -179,6 +181,8 @@ type ClusterCatalogStatus struct {
179181
// contents. This could occur when we've initially fetched the latest contents from the source for this catalog and when polling for changes
180182
// to the contents we identify that there are updates to the contents.
181183
//
184+
// +patchMergeKey=type
185+
// +patchStrategy=merge
182186
// +listType=map
183187
// +listMapKey=type
184188
// +optional
@@ -214,7 +218,7 @@ type ClusterCatalogURLs struct {
214218
//
215219
// As the needs of users and clients of the evolve, new endpoints may be added.
216220
//
217-
// +kubebuilder:validation:Required
221+
// +required
218222
// +kubebuilder:validation:MaxLength:=525
219223
// +kubebuilder:validation:XValidation:rule="isURL(self)",message="must be a valid URL"
220224
// +kubebuilder:validation:XValidation:rule="isURL(self) ? (url(self).getScheme() == \"http\" || url(self).getScheme() == \"https\") : true",message="scheme must be either http or https"
@@ -236,7 +240,7 @@ type CatalogSource struct {
236240
//
237241
// +unionDiscriminator
238242
// +kubebuilder:validation:Enum:="Image"
239-
// +kubebuilder:validation:Required
243+
// +required
240244
Type SourceType `json:"type"`
241245
// image is used to configure how catalog contents are sourced from an OCI image.
242246
// This field is required when type is Image, and forbidden otherwise.
@@ -258,19 +262,20 @@ type ResolvedCatalogSource struct {
258262
//
259263
// +unionDiscriminator
260264
// +kubebuilder:validation:Enum:="Image"
261-
// +kubebuilder:validation:Required
265+
// +required
262266
Type SourceType `json:"type"`
263267
// image is a field containing resolution information for a catalog sourced from an image.
264268
// This field must be set when type is Image, and forbidden otherwise.
265-
Image *ResolvedImageSource `json:"image"`
269+
// +optional
270+
Image *ResolvedImageSource `json:"image,omitempty"`
266271
}
267272

268273
// ResolvedImageSource provides information about the resolved source of a Catalog sourced from an image.
269274
type ResolvedImageSource struct {
270275
// ref contains the resolved image digest-based reference.
271276
// The digest format is used so users can use other tooling to fetch the exact
272277
// OCI manifests that were used to extract the catalog contents.
273-
// +kubebuilder:validation:Required
278+
// +required
274279
// +kubebuilder:validation:MaxLength:=1000
275280
// +kubebuilder:validation:XValidation:rule="self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\\\b')",message="must start with a valid domain. valid domains must be alphanumeric characters (lowercase and uppercase) separated by the \".\" character."
276281
// +kubebuilder:validation:XValidation:rule="self.find('(\\\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') != \"\"",message="a valid name is required. valid names must contain lowercase alphanumeric characters separated only by the \".\", \"_\", \"__\", \"-\" characters."
@@ -325,7 +330,7 @@ type ImageSource struct {
325330
// An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05"
326331
// An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest"
327332
//
328-
// +kubebuilder:validation:Required
333+
// +required
329334
// +kubebuilder:validation:MaxLength:=1000
330335
// +kubebuilder:validation:XValidation:rule="self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\\\b')",message="must start with a valid domain. valid domains must be alphanumeric characters (lowercase and uppercase) separated by the \".\" character."
331336
// +kubebuilder:validation:XValidation:rule="self.find('(\\\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') != \"\"",message="a valid name is required. valid names must contain lowercase alphanumeric characters separated only by the \".\", \"_\", \"__\", \"-\" characters."
@@ -344,7 +349,7 @@ type ImageSource struct {
344349
// When omitted, the image will not be polled for new content.
345350
// +kubebuilder:validation:Minimum:=1
346351
// +optional
347-
PollIntervalMinutes *int `json:"pollIntervalMinutes,omitempty"`
352+
PollIntervalMinutes *int `json:"pollIntervalMinutes,omitempty"` //nolint: kubeapilinter
348353
}
349354

350355
func init() {

api/v1/clusterextension_types.go

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ type ClusterExtensionSpec struct {
5959
// +kubebuilder:validation:MaxLength:=63
6060
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="namespace is immutable"
6161
// +kubebuilder:validation:XValidation:rule="self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$\")",message="namespace must be a valid DNS1123 label"
62-
// +kubebuilder:validation:Required
62+
// +required
6363
Namespace string `json:"namespace"`
6464

6565
// serviceAccount is a reference to a ServiceAccount used to perform all interactions
@@ -68,7 +68,7 @@ type ClusterExtensionSpec struct {
6868
// The ServiceAccount must exist in the namespace referenced in the spec.
6969
// serviceAccount is required.
7070
//
71-
// +kubebuilder:validation:Required
71+
// +required
7272
ServiceAccount ServiceAccountReference `json:"serviceAccount"`
7373

7474
// source is a required field which selects the installation source of content
@@ -84,7 +84,7 @@ type ClusterExtensionSpec struct {
8484
// catalog:
8585
// packageName: example-package
8686
//
87-
// +kubebuilder:validation:Required
87+
// +required
8888
Source SourceConfig `json:"source"`
8989

9090
// install is an optional field used to configure the installation options
@@ -112,7 +112,7 @@ type SourceConfig struct {
112112
//
113113
// +unionDiscriminator
114114
// +kubebuilder:validation:Enum:="Catalog"
115-
// +kubebuilder:validation:Required
115+
// +required
116116
SourceType string `json:"sourceType"`
117117

118118
// catalog is used to configure how information is sourced from a catalog.
@@ -162,11 +162,10 @@ type CatalogFilter struct {
162162
//
163163
// [RFC 1123]: https://tools.ietf.org/html/rfc1123
164164
//
165-
// +kubebuilder:validation.Required
165+
// +required
166166
// +kubebuilder:validation:MaxLength:=253
167167
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="packageName is immutable"
168168
// +kubebuilder:validation:XValidation:rule="self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$\")",message="packageName must be a valid DNS1123 subdomain. It must contain only lowercase alphanumeric characters, hyphens (-) or periods (.), start and end with an alphanumeric character, and be no longer than 253 characters"
169-
// +kubebuilder:validation:Required
170169
PackageName string `json:"packageName"`
171170

172171
// version is an optional semver constraint (a specific version or range of versions). When unspecified, the latest version available will be installed.
@@ -244,6 +243,7 @@ type CatalogFilter struct {
244243
// For more information on semver, please see https://semver.org/
245244
//
246245
// +kubebuilder:validation:MaxLength:=64
246+
// +kubebuilder:validation:MinLength:=1
247247
// +kubebuilder:validation:XValidation:rule="self.matches(\"^(\\\\s*(=||!=|>|<|>=|=>|<=|=<|~|~>|\\\\^)\\\\s*(v?(0|[1-9]\\\\d*|[x|X|\\\\*])(\\\\.(0|[1-9]\\\\d*|x|X|\\\\*]))?(\\\\.(0|[1-9]\\\\d*|x|X|\\\\*))?(-([0-9A-Za-z\\\\-]+(\\\\.[0-9A-Za-z\\\\-]+)*))?(\\\\+([0-9A-Za-z\\\\-]+(\\\\.[0-9A-Za-z\\\\-]+)*))?)\\\\s*)((?:\\\\s+|,\\\\s*|\\\\s*\\\\|\\\\|\\\\s*)(=||!=|>|<|>=|=>|<=|=<|~|~>|\\\\^)\\\\s*(v?(0|[1-9]\\\\d*|x|X|\\\\*])(\\\\.(0|[1-9]\\\\d*|x|X|\\\\*))?(\\\\.(0|[1-9]\\\\d*|x|X|\\\\*]))?(-([0-9A-Za-z\\\\-]+(\\\\.[0-9A-Za-z\\\\-]+)*))?(\\\\+([0-9A-Za-z\\\\-]+(\\\\.[0-9A-Za-z\\\\-]+)*))?)\\\\s*)*$\")",message="invalid version expression"
248248
// +optional
249249
Version string `json:"version,omitempty"`
@@ -356,7 +356,7 @@ type ServiceAccountReference struct {
356356
// +kubebuilder:validation:MaxLength:=253
357357
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="name is immutable"
358358
// +kubebuilder:validation:XValidation:rule="self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$\")",message="name must be a valid DNS1123 subdomain. It must contain only lowercase alphanumeric characters, hyphens (-) or periods (.), start and end with an alphanumeric character, and be no longer than 253 characters"
359-
// +kubebuilder:validation:Required
359+
// +required
360360
Name string `json:"name"`
361361
}
362362

@@ -369,7 +369,8 @@ type PreflightConfig struct {
369369
//
370370
// The CRD Upgrade Safety pre-flight check safeguards from unintended
371371
// consequences of upgrading a CRD, such as data loss.
372-
CRDUpgradeSafety *CRDUpgradeSafetyPreflightConfig `json:"crdUpgradeSafety"`
372+
// +optional
373+
CRDUpgradeSafety *CRDUpgradeSafetyPreflightConfig `json:"crdUpgradeSafety,omitempty"`
373374
}
374375

375376
// CRDUpgradeSafetyPreflightConfig is the configuration for CRD upgrade safety preflight check.
@@ -386,7 +387,7 @@ type CRDUpgradeSafetyPreflightConfig struct {
386387
// performing an upgrade operation.
387388
//
388389
// +kubebuilder:validation:Enum:="None";"Strict"
389-
// +kubebuilder:validation:Required
390+
// +required
390391
Enforcement CRDUpgradeSafetyEnforcement `json:"enforcement"`
391392
}
392393

@@ -411,20 +412,21 @@ type BundleMetadata struct {
411412
// hyphens (-) or periods (.), start and end with an alphanumeric character,
412413
// and be no longer than 253 characters.
413414
//
414-
// +kubebuilder:validation:Required
415+
// +required
415416
// +kubebuilder:validation:XValidation:rule="self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$\")",message="packageName must be a valid DNS1123 subdomain. It must contain only lowercase alphanumeric characters, hyphens (-) or periods (.), start and end with an alphanumeric character, and be no longer than 253 characters"
416417
Name string `json:"name"`
417418

418419
// version is a required field and is a reference to the version that this bundle represents
419420
// version follows the semantic versioning standard as defined in https://semver.org/.
420421
//
421-
// +kubebuilder:validation:Required
422+
// +required
422423
// +kubebuilder:validation:XValidation:rule="self.matches(\"^([0-9]+)(\\\\.[0-9]+)?(\\\\.[0-9]+)?(-([-0-9A-Za-z]+(\\\\.[-0-9A-Za-z]+)*))?(\\\\+([-0-9A-Za-z]+(-\\\\.[-0-9A-Za-z]+)*))?\")",message="version must be well-formed semver"
423424
Version string `json:"version"`
424425
}
425426

426427
// ClusterExtensionStatus defines the observed state of a ClusterExtension.
427428
type ClusterExtensionStatus struct {
429+
// conditions represents the observed state of the cluster extension.
428430
// The set of condition types which apply to all spec.source variations are Installed and Progressing.
429431
//
430432
// The Installed condition represents whether or not the bundle has been installed for this ClusterExtension.
@@ -463,7 +465,7 @@ type ClusterExtensionInstallStatus struct {
463465
// A "bundle" is a versioned set of content that represents the resources that
464466
// need to be applied to a cluster to install a package.
465467
//
466-
// +kubebuilder:validation:Required
468+
// +required
467469
Bundle BundleMetadata `json:"bundle"`
468470
}
469471

@@ -478,16 +480,20 @@ type ClusterExtensionInstallStatus struct {
478480

479481
// ClusterExtension is the Schema for the clusterextensions API
480482
type ClusterExtension struct {
481-
metav1.TypeMeta `json:",inline"`
482-
metav1.ObjectMeta `json:"metadata,omitempty"`
483+
// +optional
484+
metav1.TypeMeta `json:",inline"`
483485

484-
// spec is an optional field that defines the desired state of the ClusterExtension.
486+
// metadata is the object metadata
485487
// +optional
486-
Spec ClusterExtensionSpec `json:"spec,omitempty"`
488+
metav1.ObjectMeta `json:"metadata,omitempty"`
489+
490+
// spec is a required field that defines the desired state of the ClusterExtension.
491+
// +required
492+
Spec ClusterExtensionSpec `json:"spec"`
487493

488494
// status is an optional field that defines the observed state of the ClusterExtension.
489495
// +optional
490-
Status ClusterExtensionStatus `json:"status,omitempty"`
496+
Status ClusterExtensionStatus `json:"status,omitempty"` //nolint: kubeapilinter
491497
}
492498

493499
// +kubebuilder:object:root=true
@@ -501,7 +507,7 @@ type ClusterExtensionList struct {
501507

502508
// items is a required list of ClusterExtension objects.
503509
//
504-
// +kubebuilder:validation:Required
510+
// +required
505511
Items []ClusterExtension `json:"items"`
506512
}
507513

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ require (
124124
github.com/gogo/protobuf v1.3.2 // indirect
125125
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
126126
github.com/golang/protobuf v1.5.4 // indirect
127+
github.com/golangci/plugin-module-register v0.1.1 // indirect
127128
github.com/google/btree v1.1.3 // indirect
128129
github.com/google/cel-go v0.25.0 // indirect
129130
github.com/google/gnostic-models v0.6.9 // indirect
@@ -251,6 +252,7 @@ require (
251252
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 // indirect
252253
sigs.k8s.io/gateway-api v1.1.0 // indirect
253254
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
255+
sigs.k8s.io/kube-api-linter v0.0.0-20250626111229-e719da12d840 // indirect
254256
sigs.k8s.io/kustomize/api v0.19.0 // indirect
255257
sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect
256258
sigs.k8s.io/randfill v1.0.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
224224
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
225225
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
226226
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
227+
github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c=
228+
github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc=
227229
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
228230
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
229231
github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY=
@@ -792,6 +794,8 @@ sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM=
792794
sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs=
793795
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
794796
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
797+
sigs.k8s.io/kube-api-linter v0.0.0-20250626111229-e719da12d840 h1:zYPk3+59kzZB2HurKhyNnqYQ2ZhskB5blDJEb/TNA9E=
798+
sigs.k8s.io/kube-api-linter v0.0.0-20250626111229-e719da12d840/go.mod h1:eLCPJVcvhVcNkLOGu2IFzkF5ZpdNjrm+azKaxS+x4IQ=
795799
sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ=
796800
sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o=
797801
sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA=

0 commit comments

Comments
 (0)