Skip to content

Commit c06f54a

Browse files
fix(controller): decode old object for delete requests (#1787)
* fix(controller): decode old object for delete requests Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> --------- Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
1 parent cd5e2a8 commit c06f54a

34 files changed

+220
-186
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ dev-setup:
150150
--set 'crds.install=true' \
151151
--set 'crds.exclusive=true'\
152152
--set 'crds.createConfig=true'\
153+
--set "tls.enableController=false"\
153154
--set "webhooks.exclusive=true"\
154155
--set "webhooks.hooks.nodes.enabled=true"\
155156
--set "webhooks.service.url=$${WEBHOOK_URL}" \
@@ -415,7 +416,7 @@ nwa:
415416
$(call go-install-tool,$(NWA),github.com/$(NWA_LOOKUP)@$(NWA_VERSION))
416417

417418
GOLANGCI_LINT := $(LOCALBIN)/golangci-lint
418-
GOLANGCI_LINT_VERSION := v2.5.0
419+
GOLANGCI_LINT_VERSION := v2.7.2
419420
GOLANGCI_LINT_LOOKUP := golangci/golangci-lint
420421
golangci-lint: ## Download golangci-lint locally if necessary.
421422
@test -s $(GOLANGCI_LINT) && $(GOLANGCI_LINT) -h | grep -q $(GOLANGCI_LINT_VERSION) || \

api/v1beta1/tenant_types.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,21 @@ type TenantSpec struct {
2020
// Specifies the allowed StorageClasses assigned to the Tenant. Capsule assures that all PersistentVolumeClaim resources created in the Tenant can use only one of the allowed StorageClasses. Optional.
2121
StorageClasses *api.AllowedListSpec `json:"storageClasses,omitempty"`
2222
// Specifies options for the Ingress resources, such as allowed hostnames and IngressClass. Optional.
23-
IngressOptions IngressOptions `json:"ingressOptions,omitempty"`
23+
// +optional
24+
IngressOptions IngressOptions `json:"ingressOptions,omitzero"`
2425
// Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
2526
ContainerRegistries *api.AllowedListSpec `json:"containerRegistries,omitempty"`
2627
// Specifies the label to control the placement of pods on a given pool of worker nodes. All namespaces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
2728
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
2829
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
29-
NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitempty"`
30+
// +optional
31+
NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitzero"`
3032
// Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
31-
LimitRanges api.LimitRangesSpec `json:"limitRanges,omitempty"`
33+
// +optional
34+
LimitRanges api.LimitRangesSpec `json:"limitRanges,omitzero"`
3235
// Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional.
33-
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
36+
// +optional
37+
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitzero"`
3438
// Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
3539
AdditionalRoleBindings []api.AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
3640
// Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
@@ -50,11 +54,13 @@ type TenantSpec struct {
5054

5155
// Tenant is the Schema for the tenants API.
5256
type Tenant struct {
53-
metav1.TypeMeta `json:",inline"`
54-
metav1.ObjectMeta `json:"metadata,omitempty"`
57+
metav1.TypeMeta `json:",inline"`
58+
// +optional
59+
metav1.ObjectMeta `json:"metadata,omitzero"`
5560

56-
Spec TenantSpec `json:"spec,omitempty"`
57-
Status TenantStatus `json:"status,omitempty"`
61+
Spec TenantSpec `json:"spec"`
62+
// +optional
63+
Status TenantStatus `json:"status,omitzero"`
5864
}
5965

6066
func (in *Tenant) Hub() {}
@@ -64,7 +70,8 @@ func (in *Tenant) Hub() {}
6470
// TenantList contains a list of Tenant.
6571
type TenantList struct {
6672
metav1.TypeMeta `json:",inline"`
67-
metav1.ListMeta `json:"metadata,omitempty"`
73+
// +optional
74+
metav1.ListMeta `json:"metadata,omitzero"`
6875

6976
Items []Tenant `json:"items"`
7077
}

api/v1beta2/capsuleconfiguration_types.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,14 @@ type CapsuleConfigurationSpec struct {
4040
// Allows to set different name rather than the canonical one for the Capsule configuration objects,
4141
// such as webhook secret or configurations.
4242
// +kubebuilder:default={TLSSecretName:"capsule-tls",mutatingWebhookConfigurationName:"capsule-mutating-webhook-configuration",validatingWebhookConfigurationName:"capsule-validating-webhook-configuration"}
43-
CapsuleResources CapsuleResources `json:"overrides,omitempty"`
43+
// +optional
44+
CapsuleResources CapsuleResources `json:"overrides,omitzero"`
4445
// Allows to set the forbidden metadata for the worker nodes that could be patched by a Tenant.
4546
// This applies only if the Tenant has an active NodeSelector, and the Owner have right to patch their nodes.
4647
NodeMetadata *NodeMetadata `json:"nodeMetadata,omitempty"`
4748
// Toggles the TLS reconciler, the controller that is able to generate CA and certificates for the webhooks
4849
// when not using an already provided CA and certificate, or when these are managed externally with Vault, or cert-manager.
49-
// +kubebuilder:default=true
50+
// +kubebuilder:default=false
5051
EnableTLSReconciler bool `json:"enableTLSReconciler"` //nolint:tagliatelle
5152
// Define entities which can act as Administrators in the capsule construct
5253
// These entities are automatically owners for all existing tenants. Meaning they can add namespaces to any tenant. However they must be specific by using the capsule label
@@ -57,9 +58,11 @@ type CapsuleConfigurationSpec struct {
5758

5859
type NodeMetadata struct {
5960
// Define the labels that a Tenant Owner cannot set for their nodes.
60-
ForbiddenLabels api.ForbiddenListSpec `json:"forbiddenLabels"`
61+
// +optional
62+
ForbiddenLabels api.ForbiddenListSpec `json:"forbiddenLabels,omitzero"`
6163
// Define the annotations that a Tenant Owner cannot set for their nodes.
62-
ForbiddenAnnotations api.ForbiddenListSpec `json:"forbiddenAnnotations"`
64+
// +optional
65+
ForbiddenAnnotations api.ForbiddenListSpec `json:"forbiddenAnnotations,omitzero"`
6366
}
6467

6568
type CapsuleResources struct {
@@ -81,18 +84,20 @@ type CapsuleResources struct {
8184

8285
// CapsuleConfiguration is the Schema for the Capsule configuration API.
8386
type CapsuleConfiguration struct {
84-
metav1.TypeMeta `json:",inline"`
85-
metav1.ObjectMeta `json:"metadata,omitempty"`
87+
metav1.TypeMeta `json:",inline"`
88+
89+
// +optional
90+
metav1.ObjectMeta `json:"metadata,omitzero"`
8691

87-
Spec CapsuleConfigurationSpec `json:"spec,omitempty"`
92+
Spec CapsuleConfigurationSpec `json:"spec"`
8893
}
8994

9095
// +kubebuilder:object:root=true
9196

9297
// CapsuleConfigurationList contains a list of CapsuleConfiguration.
9398
type CapsuleConfigurationList struct {
9499
metav1.TypeMeta `json:",inline"`
95-
metav1.ListMeta `json:"metadata,omitempty"`
100+
metav1.ListMeta `json:"metadata,omitzero"`
96101

97102
Items []CapsuleConfiguration `json:"items"`
98103
}

api/v1beta2/namespace_options.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ type NamespaceOptions struct {
1818
// Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant via a list. Optional.
1919
AdditionalMetadataList []api.AdditionalMetadataSelectorSpec `json:"additionalMetadataList,omitempty"`
2020
// Define the labels that a Tenant Owner cannot set for their Namespace resources.
21-
ForbiddenLabels api.ForbiddenListSpec `json:"forbiddenLabels,omitempty"`
21+
// +optional
22+
ForbiddenLabels api.ForbiddenListSpec `json:"forbiddenLabels,omitzero"`
2223
// Define the annotations that a Tenant Owner cannot set for their Namespace resources.
23-
ForbiddenAnnotations api.ForbiddenListSpec `json:"forbiddenAnnotations,omitempty"`
24+
// +optional
25+
ForbiddenAnnotations api.ForbiddenListSpec `json:"forbiddenAnnotations,omitzero"`
2426
// If enabled only metadata from additionalMetadata is reconciled to the namespaces.
2527
//+kubebuilder:default:=false
2628
ManagedMetadataOnly bool `json:"managedMetadataOnly,omitempty"`

api/v1beta2/resourcepool_status.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ type ResourcePoolStatus struct {
2121
// Namespaces which are considered for claims
2222
Namespaces []string `json:"namespaces,omitempty"`
2323
// Tracks the quotas for the Resource.
24-
Claims ResourcePoolNamespaceClaimsStatus `json:"claims,omitempty"`
24+
// +optional
25+
Claims ResourcePoolNamespaceClaimsStatus `json:"claims,omitzero"`
2526
// Tracks the Usage from Claimed against what has been granted from the pool
26-
Allocation ResourcePoolQuotaStatus `json:"allocation,omitempty"`
27+
// +optional
28+
Allocation ResourcePoolQuotaStatus `json:"allocation,omitzero"`
2729
// Exhaustions from claims associated with the pool
2830
Exhaustions map[string]api.PoolExhaustionResource `json:"exhaustions,omitempty"`
2931
}

api/v1beta2/resourcepool_types.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ type ResourcePoolSpec struct {
1818
Quota corev1.ResourceQuotaSpec `json:"quota"`
1919
// The Defaults given for each namespace, the default is not counted towards the total allocation
2020
// When you use claims it's recommended to provision Defaults as the prevent the scheduling of any resources
21-
Defaults corev1.ResourceList `json:"defaults,omitempty"`
21+
// +optional
22+
Defaults corev1.ResourceList `json:"defaults,omitzero"`
2223
// Additional Configuration
2324
//+kubebuilder:default:={}
24-
Config ResourcePoolSpecConfiguration `json:"config,omitempty"`
25+
// +optional
26+
Config ResourcePoolSpecConfiguration `json:"config,omitzero"`
2527
}
2628

2729
type ResourcePoolSpecConfiguration struct {
@@ -55,19 +57,25 @@ type ResourcePoolSpecConfiguration struct {
5557
// it's up the group of users within these namespaces, to manage the resources they consume per namespace. Each Resourcepool provisions a ResourceQuotainto all the selected namespaces. Then essentially the ResourcePoolClaims, when they can be assigned to the ResourcePool stack resources on top of that
5658
// ResourceQuota based on the namspace, where the ResourcePoolClaim was made from.
5759
type ResourcePool struct {
58-
metav1.TypeMeta `json:",inline"`
59-
metav1.ObjectMeta `json:"metadata,omitempty"`
60+
metav1.TypeMeta `json:",inline"`
61+
62+
// +optional
63+
metav1.ObjectMeta `json:"metadata,omitzero"`
6064

61-
Spec ResourcePoolSpec `json:"spec,omitempty"`
62-
Status ResourcePoolStatus `json:"status,omitempty"`
65+
Spec ResourcePoolSpec `json:"spec"`
66+
67+
// +optional
68+
Status ResourcePoolStatus `json:"status,omitzero"`
6369
}
6470

6571
// +kubebuilder:object:root=true
6672

6773
// ResourcePoolList contains a list of ResourcePool.
6874
type ResourcePoolList struct {
6975
metav1.TypeMeta `json:",inline"`
70-
metav1.ListMeta `json:"metadata,omitempty"`
76+
77+
// +optional
78+
metav1.ListMeta `json:"metadata,omitzero"`
7179

7280
Items []ResourcePool `json:"items"`
7381
}

api/v1beta2/resourcepoolclaim_types.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ type ResourcePoolClaimSpec struct {
2222
// ResourceQuotaClaimStatus defines the observed state of ResourceQuotaClaim.
2323
type ResourcePoolClaimStatus struct {
2424
// Reference to the GlobalQuota being claimed from
25-
Pool api.StatusNameUID `json:"pool,omitempty"`
25+
// +optional
26+
Pool api.StatusNameUID `json:"pool,omitzero"`
2627
// Condtion for this resource claim
27-
Condition metav1.Condition `json:"condition,omitempty"`
28+
// +optional
29+
Condition metav1.Condition `json:"condition,omitzero"`
2830
}
2931

3032
// +kubebuilder:object:root=true
@@ -37,19 +39,25 @@ type ResourcePoolClaimStatus struct {
3739

3840
// ResourcePoolClaim is the Schema for the resourcepoolclaims API.
3941
type ResourcePoolClaim struct {
40-
metav1.TypeMeta `json:",inline"`
41-
metav1.ObjectMeta `json:"metadata,omitempty"`
42+
metav1.TypeMeta `json:",inline"`
43+
44+
// +optional
45+
metav1.ObjectMeta `json:"metadata,omitzero"`
4246

43-
Spec ResourcePoolClaimSpec `json:"spec,omitempty"`
44-
Status ResourcePoolClaimStatus `json:"status,omitempty"`
47+
Spec ResourcePoolClaimSpec `json:"spec"`
48+
49+
// +optional
50+
Status ResourcePoolClaimStatus `json:"status,omitzero"`
4551
}
4652

4753
// +kubebuilder:object:root=true
4854

4955
// ResourceQuotaClaimList contains a list of ResourceQuotaClaim.
5056
type ResourcePoolClaimList struct {
5157
metav1.TypeMeta `json:",inline"`
52-
metav1.ListMeta `json:"metadata,omitempty"`
58+
59+
// +optional
60+
metav1.ListMeta `json:"metadata,omitzero"`
5361

5462
Items []ResourcePoolClaim `json:"items"`
5563
}

api/v1beta2/tenant_status.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ type TenantStatusNamespaceMetadata struct {
5858

5959
type TenantAvailableStatus struct {
6060
// Available Class Types within Tenant
61-
Classes TenantAvailableClassesStatus `json:"classes,omitempty"`
61+
// +optional
62+
Classes TenantAvailableClassesStatus `json:"classes,omitzero"`
6263
}
6364

6465
type TenantAvailableClassesStatus struct {

api/v1beta2/tenant_types.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import (
1616
// TenantSpec defines the desired state of Tenant.
1717
type TenantSpec struct {
1818
// Specify Permissions for the Tenant.
19-
Permissions Permissions `json:"permissions,omitempty"`
19+
// +optional
20+
Permissions Permissions `json:"permissions,omitzero"`
2021
// Specifies the owners of the Tenant.
2122
// Optional
2223
Owners api.OwnerListSpec `json:"owners,omitempty"`
@@ -32,21 +33,25 @@ type TenantSpec struct {
3233
// Optional.
3334
StorageClasses *api.DefaultAllowedListSpec `json:"storageClasses,omitempty"`
3435
// Specifies options for the Ingress resources, such as allowed hostnames and IngressClass. Optional.
35-
IngressOptions IngressOptions `json:"ingressOptions,omitempty"`
36+
// +optional
37+
IngressOptions IngressOptions `json:"ingressOptions,omitzero"`
3638
// Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
3739
ContainerRegistries *api.AllowedListSpec `json:"containerRegistries,omitempty"`
3840
// Specifies the label to control the placement of pods on a given pool of worker nodes. All namespaces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
3941
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
4042
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
4143
//
4244
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
43-
NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitempty"`
45+
// +optional
46+
NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitzero"`
4447
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
4548
//
4649
// Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
47-
LimitRanges api.LimitRangesSpec `json:"limitRanges,omitempty"`
50+
// +optional
51+
LimitRanges api.LimitRangesSpec `json:"limitRanges,omitzero"`
4852
// Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional.
49-
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
53+
// +optional
54+
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitzero"`
5055
// Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
5156
AdditionalRoleBindings []api.AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
5257
// Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
@@ -63,7 +68,8 @@ type TenantSpec struct {
6368
// Specifies options for the DeviceClass resources.
6469
DeviceClasses *api.SelectorAllowedListSpec `json:"deviceClasses,omitempty"`
6570
// Specifies options for the GatewayClass resources.
66-
GatewayOptions GatewayOptions `json:"gatewayOptions,omitempty"`
71+
// +optional
72+
GatewayOptions GatewayOptions `json:"gatewayOptions,omitzero"`
6773
// Toggling the Tenant resources cordoning, when enable resources cannot be deleted.
6874
//+kubebuilder:default:=false
6975
Cordoned bool `json:"cordoned,omitempty"`
@@ -110,11 +116,15 @@ func (p *Permissions) ListMatchingOwners(
110116
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
111117
// Tenant is the Schema for the tenants API.
112118
type Tenant struct {
113-
metav1.TypeMeta `json:",inline"`
114-
metav1.ObjectMeta `json:"metadata,omitempty"`
119+
metav1.TypeMeta `json:",inline"`
120+
121+
// +optional
122+
metav1.ObjectMeta `json:"metadata,omitzero"`
123+
124+
Spec TenantSpec `json:"spec"`
115125

116-
Spec TenantSpec `json:"spec,omitempty"`
117-
Status TenantStatus `json:"status,omitempty"`
126+
// +optional
127+
Status TenantStatus `json:"status,omitzero"`
118128
}
119129

120130
func (in *Tenant) GetNamespaces() (res []string) {
@@ -130,7 +140,7 @@ func (in *Tenant) GetNamespaces() (res []string) {
130140
// TenantList contains a list of Tenant.
131141
type TenantList struct {
132142
metav1.TypeMeta `json:",inline"`
133-
metav1.ListMeta `json:"metadata,omitempty"`
143+
metav1.ListMeta `json:"metadata,omitzero"`
134144

135145
Items []Tenant `json:"items"`
136146
}

api/v1beta2/tenantresource_global.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@ type GlobalTenantResourceSpec struct {
1313
TenantResourceSpec `json:",inline"`
1414

1515
// Defines the Tenant selector used target the tenants on which resources must be propagated.
16-
TenantSelector metav1.LabelSelector `json:"tenantSelector,omitempty"`
16+
// +optional
17+
TenantSelector metav1.LabelSelector `json:"tenantSelector,omitzero"`
1718
}
1819

1920
// GlobalTenantResourceStatus defines the observed state of GlobalTenantResource.
2021
type GlobalTenantResourceStatus struct {
2122
// List of Tenants addressed by the GlobalTenantResource.
2223
SelectedTenants []string `json:"selectedTenants"`
2324
// List of the replicated resources for the given TenantResource.
24-
ProcessedItems ProcessedItems `json:"processedItems"`
25+
ProcessedItems ProcessedItems `json:"processedItems,omitzero"`
2526
}
2627

2728
type ProcessedItems []ObjectReferenceStatus
@@ -42,19 +43,23 @@ func (p *ProcessedItems) AsSet() sets.Set[string] {
4243

4344
// GlobalTenantResource allows to propagate resource replications to a specific subset of Tenant resources.
4445
type GlobalTenantResource struct {
45-
metav1.TypeMeta `json:",inline"`
46-
metav1.ObjectMeta `json:"metadata,omitempty"`
46+
metav1.TypeMeta `json:",inline"`
47+
48+
// +optional
49+
metav1.ObjectMeta `json:"metadata,omitzero"`
50+
51+
Spec GlobalTenantResourceSpec `json:"spec"`
4752

48-
Spec GlobalTenantResourceSpec `json:"spec,omitempty"`
49-
Status GlobalTenantResourceStatus `json:"status,omitempty"`
53+
// +optional
54+
Status GlobalTenantResourceStatus `json:"status,omitzero"`
5055
}
5156

5257
// +kubebuilder:object:root=true
5358

5459
// GlobalTenantResourceList contains a list of GlobalTenantResource.
5560
type GlobalTenantResourceList struct {
5661
metav1.TypeMeta `json:",inline"`
57-
metav1.ListMeta `json:"metadata,omitempty"`
62+
metav1.ListMeta `json:"metadata,omitzero"`
5863

5964
Items []GlobalTenantResource `json:"items"`
6065
}

0 commit comments

Comments
 (0)