Skip to content

Commit a42b4a1

Browse files
authored
trace sdk.go resource manager methods (#98)
Updates the sdk.go-specific templates to take advantage of the new ACK runtime logger that is embedded in all Context structs passed from the ACK runtime v0.2.3 release and after. In working these log traces into the various sdkXXX methods, I took this opportunity to clean up some of the ugly template conditionals in these methods that checked to see whether an Output shape was being referenced as a variable after calling the aws-sdk-go API calls and setting the associated variable name to `_` in order to prevent `unused variable declared` compilation failures. The template code for handling this used to look like this: ``` {{ $createCode := GoCodeSetCreateOutput .CRD "resp" "ko" 1 false }} {{ if not ( Empty $createCode ) }}resp{{ else }}_{{ end }}, respErr := rm.sdkapi.{{ .CRD.Ops.Create.ExportedName }}WithContext(ctx, input) rm.metrics.RecordAPICall("CREATE", "{{ .CRD.Ops.Create.ExportedName }}", respErr) if respErr != nil { return nil, respErr } // Merge in the information we read from the API call above to the copy of // the original Kubernetes object we passed to the function ko := r.ko.DeepCopy() {{ $createCode }} ``` but now is simpler and easier to read: ``` var resp {{ .CRD.GetOutputShapeGoType .CRD.Ops.Create }} resp, err = rm.sdkapi.{{ .CRD.Ops.Create.ExportedName }}WithContext(ctx, input) rm.metrics.RecordAPICall("CREATE", "{{ .CRD.Ops.Create.ExportedName }}", err) if err != nil { return nil, err } // Merge in the information we read from the API call above to the copy of // the original Kubernetes object we passed to the function ko := desired.ko.DeepCopy() {{ GoCodeSetCreateOutput .CRD "resp" "ko" 1 false }} ``` By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 98e1eb4 commit a42b4a1

File tree

11 files changed

+151
-88
lines changed

11 files changed

+151
-88
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/aws-controllers-k8s/code-generator
33
go 1.14
44

55
require (
6-
github.com/aws-controllers-k8s/runtime v0.2.0
6+
github.com/aws-controllers-k8s/runtime v0.2.3
77
github.com/aws/aws-sdk-go v1.37.4
88
github.com/dlclark/regexp2 v1.4.0
99
// pin to v0.1.1 due to release problem with v0.1.2

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
6161
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
6262
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
6363
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
64-
github.com/aws-controllers-k8s/runtime v0.2.0 h1:gd0Kq8xGelgkZoNjr8yZbHfpvPA1R+wfMCi1lT4H8x4=
65-
github.com/aws-controllers-k8s/runtime v0.2.0/go.mod h1:xA2F18PJerBHaqrS4de1lpP7skeSMeStkmh+3x5sWvw=
64+
github.com/aws-controllers-k8s/runtime v0.2.3 h1:pDDSXOJj5QLlC9OcgnGujeocQEg5U1oqQw3kUSDefLU=
65+
github.com/aws-controllers-k8s/runtime v0.2.3/go.mod h1:xA2F18PJerBHaqrS4de1lpP7skeSMeStkmh+3x5sWvw=
6666
github.com/aws/aws-sdk-go v1.37.4 h1:tWxrpMK/oRSXVnjUzhGeCWLR00fW0WF4V4sycYPPrJ8=
6767
github.com/aws/aws-sdk-go v1.37.4/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
6868
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=

pkg/generate/ack/runtime_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package ack
1515

1616
import (
17+
"context"
1718
"testing"
1819

1920
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -23,6 +24,7 @@ import (
2324

2425
ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1"
2526
ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare"
27+
ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log"
2628
acktypes "github.com/aws-controllers-k8s/runtime/pkg/types"
2729
)
2830

@@ -82,4 +84,12 @@ func TestRuntimeDependency(t *testing.T) {
8284

8385
require.Implements((*acktypes.AWSResourceIdentifiers)(nil), new(fakeIdentifiers))
8486
require.Implements((*acktypes.AWSResourceDescriptor)(nil), new(fakeDescriptor))
87+
88+
// ACK runtime 0.2.3 introduced a new logger that is now passed into the
89+
// Context and retrievable using the `pkg/runtime/log.FromContext`
90+
// function. This function returns NoopLogger if no such logger is found
91+
// in the context, but this check here is mostly to ensure that the new
92+
// function used in ACK runtime 0.2.3 and templates in code-generator
93+
// consuming 0.2.3 are properly pinned.
94+
require.Implements((*acktypes.Logger)(nil), ackrtlog.FromContext(context.TODO()))
8595
}

pkg/generate/mq_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,17 @@ func TestMQ_Broker(t *testing.T) {
5151
require.True(found)
5252
assert.Equal("*ackv1alpha1.SecretKeyReference", passAttr.GoType)
5353
}
54+
55+
func TestMQ_GetOutputShapeGoType(t *testing.T) {
56+
assert := assert.New(t)
57+
require := require.New(t)
58+
59+
g := testutil.NewGeneratorForService(t, "mq")
60+
61+
crd := testutil.GetCRDByName(t, g, "Broker")
62+
require.NotNil(crd)
63+
64+
exp := "*svcsdkapi.CreateBrokerResponse"
65+
otype := crd.GetOutputShapeGoType(crd.Ops.Create)
66+
assert.Equal(exp, otype)
67+
}

pkg/model/crd.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,19 @@ func (r *CRD) SetOutputCustomMethodName(
365365
return &resGenConfig.SetOutputCustomMethodName
366366
}
367367

368+
// GetOutputShapeGoType returns the Go type of the supplied operation's Output
369+
// shape, renamed to use the standardized svcsdkapi alias.
370+
func (r *CRD) GetOutputShapeGoType(
371+
op *awssdkmodel.Operation,
372+
) string {
373+
if op == nil {
374+
panic("called GetOutputShapeGoType on nil operation.")
375+
}
376+
orig := op.OutputRef.GoType()
377+
// orig will contain "*<OutputShape>" with no package specifier
378+
return "*svcsdkapi." + orig[1:]
379+
}
380+
368381
// GetOutputWrapperFieldPath returns the JSON-Path of the output wrapper field
369382
// as *string for a given operation, if specified in generator config.
370383
func (r *CRD) GetOutputWrapperFieldPath(

templates/pkg/resource/manager.go.tpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ package {{ .CRD.Names.Snake }}
55
import (
66
"context"
77
"fmt"
8-
ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors"
9-
corev1 "k8s.io/api/core/v1"
108

119
ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1"
1210
ackcfg "github.com/aws-controllers-k8s/runtime/pkg/config"
1311
ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare"
12+
ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors"
1413
ackmetrics "github.com/aws-controllers-k8s/runtime/pkg/metrics"
1514
acktypes "github.com/aws-controllers-k8s/runtime/pkg/types"
1615
"github.com/aws/aws-sdk-go/aws/session"
1716
"github.com/go-logr/logr"
17+
corev1 "k8s.io/api/core/v1"
1818

1919
svcsdk "github.com/aws/aws-sdk-go/service/{{ .ServiceIDClean }}"
2020
svcsdkapi "github.com/aws/aws-sdk-go/service/{{ .ServiceIDClean }}/{{ .ServiceIDClean }}iface"

templates/pkg/resource/sdk.go.tpl

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import (
99
ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1"
1010
ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare"
1111
ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors"
12+
ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log"
1213
"github.com/aws/aws-sdk-go/aws"
1314
svcsdk "github.com/aws/aws-sdk-go/service/{{ .ServiceIDClean }}"
1415
corev1 "k8s.io/api/core/v1"
1516
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1617

1718
svcapitypes "github.com/aws-controllers-k8s/{{.ServiceIDClean }}-controller/apis/{{ .APIVersion }}"
19+
svcsdkapi "github.com/aws/aws-sdk-go/service/{{ .ServiceIDClean }}"
1820
)
1921

2022
// Hack to avoid import errors during build...
@@ -26,6 +28,7 @@ var (
2628
_ = &svcapitypes.{{ .CRD.Names.Camel }}{}
2729
_ = ackv1alpha1.AWSAccountID("")
2830
_ = &ackerr.NotFound
31+
_ = svcsdkapi.New
2932
)
3033

3134
// sdkFind returns SDK-specific information about a supplied resource
@@ -40,52 +43,57 @@ var (
4043
{{- end }}
4144

4245
// sdkCreate creates the supplied resource in the backend AWS service API and
43-
// returns a new resource with any fields in the Status field filled in
46+
// returns a copy of the resource with resource fields (in both Spec and
47+
// Status) filled in with values from the CREATE API operation's Output shape.
4448
func (rm *resourceManager) sdkCreate(
4549
ctx context.Context,
46-
r *resource,
47-
) (*resource, error) {
50+
desired *resource,
51+
) (created *resource, err error) {
52+
rlog := ackrtlog.FromContext(ctx)
53+
exit := rlog.Trace("rm.sdkCreate")
54+
defer exit(err)
55+
4856
{{- if $hookCode := Hook .CRD "sdk_create_pre_build_request" }}
4957
{{ $hookCode }}
5058
{{- end }}
51-
{{- $customMethod := .CRD.GetCustomImplementation .CRD.Ops.Create -}}
52-
{{- if $customMethod }}
53-
customResp, customRespErr := rm.{{ $customMethod }}(ctx, r)
54-
if customResp != nil || customRespErr != nil {
55-
return customResp, customRespErr
59+
{{- if $customMethod := .CRD.GetCustomImplementation .CRD.Ops.Create -}}
60+
created, err = rm.{{ $customMethod }}(ctx, desired)
61+
if created != nil || err != nil {
62+
return created, err
5663
}
5764
{{- end }}
58-
input, err := rm.newCreateRequestPayload(ctx, r)
65+
input, err := rm.newCreateRequestPayload(ctx, desired)
5966
if err != nil {
6067
return nil, err
6168
}
6269
{{- if $hookCode := Hook .CRD "sdk_create_post_build_request" }}
6370
{{ $hookCode }}
64-
{{- end }}
65-
{{ $createCode := GoCodeSetCreateOutput .CRD "resp" "ko" 1 false }}
66-
{{ if not ( Empty $createCode ) }}resp{{ else }}_{{ end }}, respErr := rm.sdkapi.{{ .CRD.Ops.Create.ExportedName }}WithContext(ctx, input)
71+
{{- end }}
72+
73+
var resp {{ .CRD.GetOutputShapeGoType .CRD.Ops.Create }}
74+
resp, err = rm.sdkapi.{{ .CRD.Ops.Create.ExportedName }}WithContext(ctx, input)
6775
{{- if $hookCode := Hook .CRD "sdk_create_post_request" }}
6876
{{ $hookCode }}
6977
{{- end }}
70-
rm.metrics.RecordAPICall("CREATE", "{{ .CRD.Ops.Create.ExportedName }}", respErr)
71-
if respErr != nil {
72-
return nil, respErr
78+
rm.metrics.RecordAPICall("CREATE", "{{ .CRD.Ops.Create.ExportedName }}", err)
79+
if err != nil {
80+
return nil, err
7381
}
7482
// Merge in the information we read from the API call above to the copy of
7583
// the original Kubernetes object we passed to the function
76-
ko := r.ko.DeepCopy()
84+
ko := desired.ko.DeepCopy()
7785
{{- if $hookCode := Hook .CRD "sdk_create_pre_set_output" }}
7886
{{ $hookCode }}
7987
{{- end }}
80-
{{ $createCode }}
88+
{{ GoCodeSetCreateOutput .CRD "resp" "ko" 1 false }}
8189
rm.setStatusDefaults(ko)
82-
{{ if $setOutputCustomMethodName := .CRD.SetOutputCustomMethodName .CRD.Ops.Create }}
83-
// custom set output from response
84-
ko, err = rm.{{ $setOutputCustomMethodName }}(ctx, r, resp, ko)
85-
if err != nil {
86-
return nil, err
87-
}
88-
{{ end }}
90+
{{- if $setOutputCustomMethodName := .CRD.SetOutputCustomMethodName .CRD.Ops.Create }}
91+
// custom set output from response
92+
ko, err = rm.{{ $setOutputCustomMethodName }}(ctx, desired, resp, ko)
93+
if err != nil {
94+
return nil, err
95+
}
96+
{{- end }}
8997
{{- if $hookCode := Hook .CRD "sdk_create_post_set_output" }}
9098
{{ $hookCode }}
9199
{{- end }}
@@ -95,8 +103,8 @@ func (rm *resourceManager) sdkCreate(
95103
// newCreateRequestPayload returns an SDK-specific struct for the HTTP request
96104
// payload of the Create API call for the resource
97105
func (rm *resourceManager) newCreateRequestPayload(
98-
ctx context.Context,
99-
r *resource,
106+
ctx context.Context,
107+
r *resource,
100108
) (*svcsdk.{{ .CRD.Ops.Create.InputRef.Shape.ShapeName }}, error) {
101109
res := &svcsdk.{{ .CRD.Ops.Create.InputRef.Shape.ShapeName }}{}
102110
{{ GoCodeSetCreateInput .CRD "r.ko" "res" 1 }}
@@ -119,31 +127,33 @@ func (rm *resourceManager) newCreateRequestPayload(
119127
func (rm *resourceManager) sdkDelete(
120128
ctx context.Context,
121129
r *resource,
122-
) error {
130+
) (err error) {
131+
rlog := ackrtlog.FromContext(ctx)
132+
exit := rlog.Trace("rm.sdkDelete")
133+
defer exit(err)
134+
123135
{{- if .CRD.Ops.Delete }}
124136
{{- if $hookCode := Hook .CRD "sdk_delete_pre_build_request" }}
125137
{{ $hookCode }}
126138
{{- end }}
127-
{{ $customMethod := .CRD.GetCustomImplementation .CRD.Ops.Delete }}
128-
{{ if $customMethod }}
129-
customRespErr := rm.{{ $customMethod }}(ctx, r)
130-
if customRespErr != nil {
131-
return customRespErr
139+
{{- if $customMethod := .CRD.GetCustomImplementation .CRD.Ops.Delete }}
140+
if err = rm.{{ $customMethod }}(ctx, r); err != nil {
141+
return err
132142
}
133-
{{ end }}
143+
{{- end }}
134144
input, err := rm.newDeleteRequestPayload(r)
135145
if err != nil {
136146
return err
137147
}
138148
{{- if $hookCode := Hook .CRD "sdk_delete_post_build_request" }}
139149
{{ $hookCode }}
140150
{{- end }}
141-
_, respErr := rm.sdkapi.{{ .CRD.Ops.Delete.Name }}WithContext(ctx, input)
142-
rm.metrics.RecordAPICall("DELETE", "{{ .CRD.Ops.Delete.Name }}", respErr)
151+
_, err = rm.sdkapi.{{ .CRD.Ops.Delete.Name }}WithContext(ctx, input)
152+
rm.metrics.RecordAPICall("DELETE", "{{ .CRD.Ops.Delete.Name }}", err)
143153
{{- if $hookCode := Hook .CRD "sdk_delete_post_request" }}
144154
{{ $hookCode }}
145155
{{- end }}
146-
return respErr
156+
return err
147157
{{- else }}
148158
// TODO(jaypipes): Figure this out...
149159
return nil
@@ -335,4 +345,4 @@ func (rm *resourceManager) handleImmutableFieldsChangedCondition(
335345

336346
return &resource{ko}
337347
}
338-
{{- end }}
348+
{{- end }}

templates/pkg/resource/sdk_find_get_attributes.go.tpl

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
func (rm *resourceManager) sdkFind(
33
ctx context.Context,
44
r *resource,
5-
) (*resource, error) {
5+
) (latest *resource, err error) {
6+
rlog := ackrtlog.FromContext(ctx)
7+
exit := rlog.Trace("rm.sdkFind")
8+
defer exit(err)
9+
610
{{- if $hookCode := Hook .CRD "sdk_get_attributes_pre_build_request" }}
711
{{ $hookCode }}
812
{{- end }}
@@ -20,23 +24,23 @@ func (rm *resourceManager) sdkFind(
2024
{{- if $hookCode := Hook .CRD "sdk_get_attributes_post_build_request" }}
2125
{{ $hookCode }}
2226
{{- end }}
23-
{{ $setCode := GoCodeGetAttributesSetOutput .CRD "resp" "ko" 1 }}
24-
{{ if not ( Empty $setCode ) }}resp{{ else }}_{{ end }}, respErr := rm.sdkapi.{{ .CRD.Ops.GetAttributes.ExportedName }}WithContext(ctx, input)
27+
var resp {{ .CRD.GetOutputShapeGoType .CRD.Ops.GetAttributes }}
28+
resp, err = rm.sdkapi.{{ .CRD.Ops.GetAttributes.ExportedName }}WithContext(ctx, input)
2529
{{- if $hookCode := Hook .CRD "sdk_get_attributes_post_request" }}
2630
{{ $hookCode }}
2731
{{- end }}
28-
rm.metrics.RecordAPICall("GET_ATTRIBUTES", "{{ .CRD.Ops.GetAttributes.ExportedName }}", respErr)
29-
if respErr != nil {
30-
if awsErr, ok := ackerr.AWSError(respErr); ok && awsErr.Code() == "{{ ResourceExceptionCode .CRD 404 }}" {{ GoCodeSetExceptionMessageCheck .CRD 404 }}{
32+
rm.metrics.RecordAPICall("GET_ATTRIBUTES", "{{ .CRD.Ops.GetAttributes.ExportedName }}", err)
33+
if err != nil {
34+
if awsErr, ok := ackerr.AWSError(err); ok && awsErr.Code() == "{{ ResourceExceptionCode .CRD 404 }}" {{ GoCodeSetExceptionMessageCheck .CRD 404 }}{
3135
return nil, ackerr.NotFound
3236
}
33-
return nil, respErr
37+
return nil, err
3438
}
3539

3640
// Merge in the information we read from the API call above to the copy of
3741
// the original Kubernetes object we passed to the function
3842
ko := r.ko.DeepCopy()
39-
{{ $setCode }}
43+
{{ GoCodeGetAttributesSetOutput .CRD "resp" "ko" 1 }}
4044
{{- if $hookCode := Hook .CRD "sdk_get_attributes_pre_set_output" }}
4145
{{ $hookCode }}
4246
{{- end }}

templates/pkg/resource/sdk_find_read_many.go.tpl

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
func (rm *resourceManager) sdkFind(
33
ctx context.Context,
44
r *resource,
5-
) (*resource, error) {
5+
) (latest *resource, err error) {
6+
rlog := ackrtlog.FromContext(ctx)
7+
exit := rlog.Trace("rm.sdkFind")
8+
defer exit(err)
9+
610
{{- if $hookCode := Hook .CRD "sdk_read_many_pre_build_request" }}
711
{{ $hookCode }}
812
{{- end }}
@@ -13,17 +17,17 @@ func (rm *resourceManager) sdkFind(
1317
{{- if $hookCode := Hook .CRD "sdk_read_many_post_build_request" }}
1418
{{ $hookCode }}
1519
{{- end }}
16-
{{ $setCode := GoCodeSetReadManyOutput .CRD "resp" "ko" 1 true }}
17-
{{ if not ( Empty $setCode ) }}resp{{ else }}_{{ end }}, respErr := rm.sdkapi.{{ .CRD.Ops.ReadMany.ExportedName }}WithContext(ctx, input)
20+
var resp {{ .CRD.GetOutputShapeGoType .CRD.Ops.ReadMany }}
21+
resp, err = rm.sdkapi.{{ .CRD.Ops.ReadMany.ExportedName }}WithContext(ctx, input)
1822
{{- if $hookCode := Hook .CRD "sdk_read_many_post_request" }}
1923
{{ $hookCode }}
2024
{{- end }}
21-
rm.metrics.RecordAPICall("READ_MANY", "{{ .CRD.Ops.ReadMany.ExportedName }}", respErr)
22-
if respErr != nil {
23-
if awsErr, ok := ackerr.AWSError(respErr); ok && awsErr.Code() == "{{ ResourceExceptionCode .CRD 404 }}" {{ GoCodeSetExceptionMessageCheck .CRD 404 }}{
25+
rm.metrics.RecordAPICall("READ_MANY", "{{ .CRD.Ops.ReadMany.ExportedName }}", err)
26+
if err != nil {
27+
if awsErr, ok := ackerr.AWSError(err); ok && awsErr.Code() == "{{ ResourceExceptionCode .CRD 404 }}" {{ GoCodeSetExceptionMessageCheck .CRD 404 }}{
2428
return nil, ackerr.NotFound
2529
}
26-
return nil, respErr
30+
return nil, err
2731
}
2832

2933
// Merge in the information we read from the API call above to the copy of
@@ -32,15 +36,15 @@ func (rm *resourceManager) sdkFind(
3236
{{- if $hookCode := Hook .CRD "sdk_read_many_pre_set_output" }}
3337
{{ $hookCode }}
3438
{{- end }}
35-
{{ $setCode }}
39+
{{ GoCodeSetReadManyOutput .CRD "resp" "ko" 1 true }}
3640
rm.setStatusDefaults(ko)
37-
{{ if $setOutputCustomMethodName := .CRD.SetOutputCustomMethodName .CRD.Ops.ReadMany }}
41+
{{- if $setOutputCustomMethodName := .CRD.SetOutputCustomMethodName .CRD.Ops.ReadMany }}
3842
// custom set output from response
3943
ko, err = rm.{{ $setOutputCustomMethodName }}(ctx, r, resp, ko)
4044
if err != nil {
4145
return nil, err
4246
}
43-
{{ end }}
47+
{{- end }}
4448
{{- if $hookCode := Hook .CRD "sdk_read_many_post_set_output" }}
4549
{{ $hookCode }}
4650
{{- end }}

0 commit comments

Comments
 (0)