Skip to content

Commit 5e31965

Browse files
committed
type Duration string -> type Duration time.Duration
Signed-off-by: Piotr Janik <piotr.janik@sap.com>
1 parent 80911c0 commit 5e31965

9 files changed

Lines changed: 138 additions & 186 deletions

File tree

api/oci/config/httpconfig_test.go

Lines changed: 60 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package config_test
22

33
import (
4+
"encoding/json"
45
"time"
56

67
. "github.com/onsi/ginkgo/v2"
@@ -11,7 +12,11 @@ import (
1112
)
1213

1314
func dur(s string) *cpi.Duration {
14-
d := cpi.Duration(s)
15+
td, err := time.ParseDuration(s)
16+
if err != nil {
17+
panic(err)
18+
}
19+
d := cpi.Duration(td)
1520
return &d
1621
}
1722

@@ -29,7 +34,7 @@ var _ = Describe("http config", func() {
2934

3035
Expect(cfg.ApplyTo(ctx.ConfigContext(), ctx)).To(Succeed())
3136
g := MustGetHTTPSettings(ctx)
32-
Expect(g.Timeout.TimeDuration()).To(HaveValue(Equal(5 * time.Minute)))
37+
Expect(time.Duration(*g.Timeout)).To(Equal(5 * time.Minute))
3338
})
3439

3540
It("applies via config context", func() {
@@ -38,7 +43,7 @@ var _ = Describe("http config", func() {
3843

3944
Expect(ctx.ConfigContext().ApplyConfig(cfg, "programmatic")).To(Succeed())
4045
g := MustGetHTTPSettings(ctx)
41-
Expect(g.Timeout.TimeDuration()).To(HaveValue(Equal(30 * time.Second)))
46+
Expect(time.Duration(*g.Timeout)).To(Equal(30 * time.Second))
4247
})
4348

4449
It("parses all fields from JSON config", func() {
@@ -49,12 +54,12 @@ var _ = Describe("http config", func() {
4954
Expect(ctx.ConfigContext().ApplyConfig(cfg, "config file")).To(Succeed())
5055

5156
g := MustGetHTTPSettings(ctx)
52-
Expect(g.Timeout.TimeDuration()).To(HaveValue(Equal(10 * time.Second)))
53-
Expect(g.TCPDialTimeout.TimeDuration()).To(HaveValue(Equal(15 * time.Second)))
54-
Expect(g.TCPKeepAlive.TimeDuration()).To(HaveValue(Equal(20 * time.Second)))
55-
Expect(g.TLSHandshakeTimeout.TimeDuration()).To(HaveValue(Equal(5 * time.Second)))
56-
Expect(g.ResponseHeaderTimeout.TimeDuration()).To(HaveValue(Equal(8 * time.Second)))
57-
Expect(g.IdleConnTimeout.TimeDuration()).To(HaveValue(Equal(45 * time.Second)))
57+
Expect(time.Duration(*g.Timeout)).To(Equal(10 * time.Second))
58+
Expect(time.Duration(*g.TCPDialTimeout)).To(Equal(15 * time.Second))
59+
Expect(time.Duration(*g.TCPKeepAlive)).To(Equal(20 * time.Second))
60+
Expect(time.Duration(*g.TLSHandshakeTimeout)).To(Equal(5 * time.Second))
61+
Expect(time.Duration(*g.ResponseHeaderTimeout)).To(Equal(8 * time.Second))
62+
Expect(time.Duration(*g.IdleConnTimeout)).To(Equal(45 * time.Second))
5863
})
5964

6065
It("successive ApplyConfig overrides only non-nil fields", func() {
@@ -71,8 +76,8 @@ var _ = Describe("http config", func() {
7176
Expect(second.ApplyTo(ctx.ConfigContext(), ctx)).To(Succeed())
7277

7378
g := MustGetHTTPSettings(ctx)
74-
Expect(g.Timeout.TimeDuration()).To(HaveValue(Equal(1 * time.Minute)))
75-
Expect(g.TCPDialTimeout.TimeDuration()).To(HaveValue(Equal(15 * time.Second)))
79+
Expect(time.Duration(*g.Timeout)).To(Equal(1 * time.Minute))
80+
Expect(time.Duration(*g.TCPDialTimeout)).To(Equal(15 * time.Second))
7681
})
7782

7883
It("applies via generic config wrapper", func() {
@@ -93,21 +98,27 @@ configurations:
9398
Expect(ctx.ConfigContext().ApplyConfig(cfg, "config file")).To(Succeed())
9499

95100
g := MustGetHTTPSettings(ctx)
96-
Expect(g.Timeout.TimeDuration()).To(HaveValue(Equal(10 * time.Second)))
97-
Expect(g.TCPDialTimeout.TimeDuration()).To(HaveValue(Equal(15 * time.Second)))
98-
Expect(g.TCPKeepAlive.TimeDuration()).To(HaveValue(Equal(20 * time.Second)))
99-
Expect(g.TLSHandshakeTimeout.TimeDuration()).To(HaveValue(Equal(5 * time.Second)))
100-
Expect(g.ResponseHeaderTimeout.TimeDuration()).To(HaveValue(Equal(8 * time.Second)))
101-
Expect(g.IdleConnTimeout.TimeDuration()).To(HaveValue(Equal(45 * time.Second)))
101+
Expect(time.Duration(*g.Timeout)).To(Equal(10 * time.Second))
102+
Expect(time.Duration(*g.TCPDialTimeout)).To(Equal(15 * time.Second))
103+
Expect(time.Duration(*g.TCPKeepAlive)).To(Equal(20 * time.Second))
104+
Expect(time.Duration(*g.TLSHandshakeTimeout)).To(Equal(5 * time.Second))
105+
Expect(time.Duration(*g.ResponseHeaderTimeout)).To(Equal(8 * time.Second))
106+
Expect(time.Duration(*g.IdleConnTimeout)).To(Equal(45 * time.Second))
102107
})
103108

104-
It("rejects invalid duration string", func() {
105-
ctx := cpi.New()
106-
raw := []byte(`{"type":"http.config.ocm.software/v1alpha1","timeout":"notaduration"}`)
107-
_, err := ctx.ConfigContext().GetConfigForData(raw, nil)
108-
Expect(err).To(HaveOccurred())
109-
Expect(err.Error()).To(ContainSubstring("invalid duration: notaduration"))
110-
})
109+
DescribeTable("rejects invalid duration values on unmarshal",
110+
func(jsonValue string, expectedErr string) {
111+
ctx := cpi.New()
112+
raw := []byte(`{"type":"http.config.ocm.software/v1alpha1","timeout":` + jsonValue + `}`)
113+
_, err := ctx.ConfigContext().GetConfigForData(raw, nil)
114+
Expect(err).To(HaveOccurred())
115+
Expect(err.Error()).To(ContainSubstring(expectedErr))
116+
},
117+
Entry("garbage string", `"notaduration"`, "expected a Go duration string"),
118+
Entry("number instead of string", `42`, "expected a Go duration string"),
119+
Entry("boolean instead of string", `true`, "expected a Go duration string"),
120+
Entry("empty string", `""`, "expected a Go duration string"),
121+
)
111122

112123
DescribeTable("rejects negative duration for timeout fields",
113124
func(field string, cfg *config.HTTPConfig) {
@@ -116,24 +127,24 @@ configurations:
116127
ContainSubstring("invalid value for " + field),
117128
))
118129
},
119-
Entry("timeout", "timeout",
130+
Entry("timeout -5m", "timeout",
120131
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{Timeout: dur("-5m")}}),
121-
Entry("tcpDialTimeout", "tcpDialTimeout",
122-
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{TCPDialTimeout: dur("-5m")}}),
123-
Entry("tlsHandshakeTimeout", "tlsHandshakeTimeout",
124-
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{TLSHandshakeTimeout: dur("-5m")}}),
125-
Entry("responseHeaderTimeout", "responseHeaderTimeout",
126-
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{ResponseHeaderTimeout: dur("-5m")}}),
127-
Entry("idleConnTimeout", "idleConnTimeout",
128-
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{IdleConnTimeout: dur("-5m")}}),
132+
Entry("tcpDialTimeout -10s", "tcpDialTimeout",
133+
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{TCPDialTimeout: dur("-10s")}}),
134+
Entry("tlsHandshakeTimeout -10h5m", "tlsHandshakeTimeout",
135+
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{TLSHandshakeTimeout: dur("-10h5m")}}),
136+
Entry("responseHeaderTimeout -1s", "responseHeaderTimeout",
137+
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{ResponseHeaderTimeout: dur("-1s")}}),
138+
Entry("idleConnTimeout -30s", "idleConnTimeout",
139+
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{IdleConnTimeout: dur("-30s")}}),
129140
)
130141

131142
It("allows negative tcpKeepAlive to disable keep-alive probes", func() {
132143
ctx := cpi.New()
133144
cfg := &config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{TCPKeepAlive: dur("-1s")}}
134145
Expect(cfg.ApplyTo(ctx.ConfigContext(), ctx)).To(Succeed())
135146
g := MustGetHTTPSettings(ctx)
136-
Expect(g.TCPKeepAlive.TimeDuration()).To(HaveValue(Equal(-1 * time.Second)))
147+
Expect(time.Duration(*g.TCPKeepAlive)).To(Equal(-1 * time.Second))
137148
})
138149

139150
DescribeTable("accepts compound duration like 1h5s",
@@ -149,44 +160,6 @@ configurations:
149160
Entry("idleConnTimeout", &config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{IdleConnTimeout: dur("1h5s")}}),
150161
)
151162

152-
DescribeTable("rejects negative compound duration -10h5m",
153-
func(field string, cfg *config.HTTPConfig) {
154-
ctx := cpi.New()
155-
Expect(cfg.ApplyTo(ctx.ConfigContext(), ctx)).To(MatchError(
156-
ContainSubstring("invalid value for " + field),
157-
))
158-
},
159-
Entry("timeout", "timeout",
160-
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{Timeout: dur("-10h5m")}}),
161-
Entry("tcpDialTimeout", "tcpDialTimeout",
162-
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{TCPDialTimeout: dur("-10h5m")}}),
163-
Entry("tlsHandshakeTimeout", "tlsHandshakeTimeout",
164-
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{TLSHandshakeTimeout: dur("-10h5m")}}),
165-
Entry("responseHeaderTimeout", "responseHeaderTimeout",
166-
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{ResponseHeaderTimeout: dur("-10h5m")}}),
167-
Entry("idleConnTimeout", "idleConnTimeout",
168-
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{IdleConnTimeout: dur("-10h5m")}}),
169-
)
170-
171-
DescribeTable("rejects -10s",
172-
func(field string, cfg *config.HTTPConfig) {
173-
ctx := cpi.New()
174-
Expect(cfg.ApplyTo(ctx.ConfigContext(), ctx)).To(MatchError(
175-
ContainSubstring("invalid value for " + field),
176-
))
177-
},
178-
Entry("timeout", "timeout",
179-
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{Timeout: dur("-10s")}}),
180-
Entry("tcpDialTimeout", "tcpDialTimeout",
181-
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{TCPDialTimeout: dur("-10s")}}),
182-
Entry("tlsHandshakeTimeout", "tlsHandshakeTimeout",
183-
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{TLSHandshakeTimeout: dur("-10s")}}),
184-
Entry("responseHeaderTimeout", "responseHeaderTimeout",
185-
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{ResponseHeaderTimeout: dur("-10s")}}),
186-
Entry("idleConnTimeout", "idleConnTimeout",
187-
&config.HTTPConfig{HTTPSettings: cpi.HTTPSettings{IdleConnTimeout: dur("-10s")}}),
188-
)
189-
190163
It("default settings are nil", func() {
191164
g := MustGetHTTPSettings(cpi.New())
192165
Expect(g.Timeout).To(BeNil())
@@ -199,9 +172,22 @@ configurations:
199172

200173
It("nil timeout returns nil not zero", func() {
201174
g := MustGetHTTPSettings(cpi.New())
202-
timeout, err := g.Timeout.TimeDuration()
175+
Expect(g.Timeout).To(BeNil())
176+
})
177+
178+
It("round-trips Duration through MarshalJSON and UnmarshalJSON", func() {
179+
original := cpi.HTTPSettings{
180+
Timeout: dur("5m30s"),
181+
TCPDialTimeout: dur("15s"),
182+
}
183+
data, err := json.Marshal(original)
203184
Expect(err).To(Succeed())
204-
Expect(timeout).To(BeNil())
185+
186+
var restored cpi.HTTPSettings
187+
Expect(json.Unmarshal(data, &restored)).To(Succeed())
188+
Expect(time.Duration(*restored.Timeout)).To(Equal(5*time.Minute + 30*time.Second))
189+
Expect(time.Duration(*restored.TCPDialTimeout)).To(Equal(15 * time.Second))
190+
Expect(restored.TCPKeepAlive).To(BeNil())
205191
})
206192
})
207193
})

api/oci/extensions/repositories/docker/client.go

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package docker
55
import (
66
"net/http"
77
"os"
8+
"time"
89

910
"github.com/docker/cli/cli/command"
1011
"github.com/docker/cli/cli/config"
@@ -43,20 +44,13 @@ func newDockerClient(dockerhost string, logger mlog.UnboundLogger, httpCfg *cpi.
4344
opts = append(opts, dockerclient.WithScheme(url.Scheme))
4445
}
4546

46-
transport, err := httpclient.NewTransport(httpCfg)
47-
if err != nil {
48-
return nil, err
49-
}
50-
timeout, err := httpCfg.Timeout.TimeDuration()
51-
if err != nil {
52-
return nil, err
53-
}
47+
transport := httpclient.NewTransport(httpCfg)
5448

5549
clnt := http.Client{
5650
Transport: logging.NewRoundTripper(transport, logger),
5751
}
58-
if timeout != nil {
59-
clnt.Timeout = *timeout
52+
if httpCfg.Timeout != nil {
53+
clnt.Timeout = time.Duration(*httpCfg.Timeout)
6054
}
6155
opts = append(opts, dockerclient.WithHTTPClient(&clnt))
6256
c, err := dockerclient.New(opts...)

api/oci/extensions/repositories/docker/repository.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ func NewRepository(ctx cpi.Context, spec *RepositorySpec) (cpi.Repository, error
2727
if err != nil {
2828
return nil, err
2929
}
30-
httpCfg := &httpSettings
31-
client, err := newDockerClient(spec.DockerHost, logger, httpCfg)
30+
client, err := newDockerClient(spec.DockerHost, logger, &httpSettings)
3231
if err != nil {
3332
return nil, err
3433
}

api/oci/extensions/repositories/ocireg/repository.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -194,19 +194,16 @@ func configureTransport(ctx cpi.Context, scheme string, creds credentials.Creden
194194
if err != nil {
195195
return nil, nil, err
196196
}
197-
httpCfg := &httpSettings
198-
baseTransport, err := httpclient.NewTransport(httpCfg)
199-
if err != nil {
200-
return nil, nil, err
201-
}
202-
timeout, err := httpCfg.Timeout.TimeDuration()
203-
if err != nil {
204-
return nil, nil, err
197+
baseTransport := httpclient.NewTransport(&httpSettings)
198+
var timeout *time.Duration
199+
if httpSettings.Timeout != nil {
200+
timeout = new(time.Duration(*httpSettings.Timeout))
205201
}
206202

207203
if scheme == "https" {
208-
//nolint:gosec // there are OCI servers (quay.io) not working with min version.
204+
//nolint:gosec // used like the default, there are OCI servers (quay.io) not working with min version.
209205
baseTransport.TLSClientConfig = &tls.Config{
206+
// MinVersion: tls.VersionTLS13,
210207
RootCAs: func() *x509.CertPool {
211208
rootCAs := rootcertsattr.Get(ctx).GetRootCertPool(true)
212209
if creds != nil {

api/oci/internal/httpsettings.go

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,50 +6,29 @@ import (
66
"time"
77
)
88

9-
// Duration is a string type representing a Go duration (e.g. "30s", "5m").
10-
// It is validated on JSON unmarshaling.
11-
type Duration string
9+
// Duration is a time.Duration that marshals to/from a human-readable
10+
// Go duration string (e.g. "30s", "5m") in JSON/YAML.
11+
type Duration time.Duration
1212

1313
// UnmarshalJSON implements the json.Unmarshaller interface.
14+
// It parses a quoted Go duration string (e.g. "30s", "1h5m") into a Duration.
1415
func (d *Duration) UnmarshalJSON(b []byte) error {
1516
var str string
1617
if err := json.Unmarshal(b, &str); err != nil {
17-
return err
18+
return fmt.Errorf("invalid duration %s: expected a Go duration string (e.g. \"30s\", \"5m\", \"1h30m\")", string(b))
1819
}
19-
if _, err := time.ParseDuration(str); err != nil {
20-
return fmt.Errorf("invalid duration: %s", str)
21-
}
22-
*d = Duration(str)
23-
return nil
24-
}
25-
26-
// TimeDuration parses the Duration string and returns a *time.Duration.
27-
// Returns (nil, nil) if d is nil or empty — callers must distinguish
28-
// nil (not configured) from zero (explicitly disabled).
29-
// Returns an error if the string is malformed.
30-
func (d *Duration) TimeDuration() (*time.Duration, error) {
31-
if d == nil || *d == "" {
32-
return nil, nil
33-
}
34-
pd, err := time.ParseDuration(string(*d))
20+
pd, err := time.ParseDuration(str)
3521
if err != nil {
36-
return nil, fmt.Errorf("invalid duration %q: %w", string(*d), err)
22+
return fmt.Errorf("invalid duration %q: expected a Go duration string (e.g. \"30s\", \"5m\", \"1h30m\")", str)
3723
}
38-
return &pd, nil
24+
*d = Duration(pd)
25+
return nil
3926
}
4027

41-
func validateNonNegative(name string, d *Duration) error {
42-
if d == nil {
43-
return nil
44-
}
45-
td, err := d.TimeDuration()
46-
if err != nil {
47-
return err
48-
}
49-
if td != nil && *td < 0 {
50-
return fmt.Errorf("invalid value for %s: %s, must be zero or positive", name, string(*d))
51-
}
52-
return nil
28+
// MarshalJSON implements the json.Marshaler interface.
29+
// It encodes the Duration as a quoted Go duration string (e.g. "30s", "1h5m").
30+
func (d Duration) MarshalJSON() ([]byte, error) {
31+
return json.Marshal(time.Duration(d).String())
5332
}
5433

5534
// HTTPSettings contains the timeout settings for HTTP clients.
@@ -99,8 +78,8 @@ func (s *HTTPSettings) Validate() error {
9978
{"responseHeaderTimeout", s.ResponseHeaderTimeout},
10079
{"idleConnTimeout", s.IdleConnTimeout},
10180
} {
102-
if err := validateNonNegative(check.name, check.val); err != nil {
103-
return err
81+
if check.val != nil && time.Duration(*check.val) < 0 {
82+
return fmt.Errorf("invalid value for %s: %s, must be zero or positive", check.name, time.Duration(*check.val))
10483
}
10584
}
10685
return nil

0 commit comments

Comments
 (0)