Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions internal/evaluator/criteria.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ package evaluator
import (
"fmt"
"regexp"
"time"

ecc "github.com/conforma/crds/api/v1alpha1"
"github.com/google/go-containerregistry/pkg/name"
log "github.com/sirupsen/logrus"

"github.com/conforma/cli/internal/timeutil"
)

// contains include/exclude items
Expand Down Expand Up @@ -179,20 +180,26 @@ func computeIncludeExclude(src ecc.Source, p ConfigProvider) (*Criteria, *Criter
func collectVolatileConfigItems(items *Criteria, volatileCriteria []ecc.VolatileCriteria, p ConfigProvider) *Criteria {
at := p.EffectiveTime()
Comment thread
st3penta marked this conversation as resolved.
for _, c := range volatileCriteria {
from, err := time.Parse(time.RFC3339, c.EffectiveOn)
if err != nil {
if c.EffectiveOn != "" {
log.Warnf("unable to parse time for criteria %q, was given %q: %v", c.Value, c.EffectiveOn, err)
from := at
if c.EffectiveOn != "" {
t, err := timeutil.ParseVolatileTime(c.EffectiveOn)
if err != nil {
log.Warnf("skipping volatile criteria %q: %v", c.Value, err)
continue
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
from = at
from = t
}
until, err := time.Parse(time.RFC3339, c.EffectiveUntil)
if err != nil {
if c.EffectiveUntil != "" {
log.Warnf("unable to parse time for criteria %q, was given %q: %v", c.Value, c.EffectiveUntil, err)

until := at
if c.EffectiveUntil != "" {
t, err := timeutil.ParseVolatileTime(c.EffectiveUntil)
if err != nil {
log.Warnf("skipping volatile criteria %q: %v", c.Value, err)
continue
}
until = at
until = t
}

if until.Compare(at) >= 0 && from.Compare(at) <= 0 {
// DEPRECATED: use c.ImageDigest instead
if c.ImageRef != "" {
Expand Down
75 changes: 72 additions & 3 deletions internal/evaluator/criteria_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ func TestCollectVolatileConfigItems(t *testing.T) {
expectedSuccess: true, // Function doesn't fail, just doesn't add items
},
{
name: "Warning scenario - invalid time formats",
name: "Warning scenario - invalid time formats skips item",
items: &Criteria{
digestItems: make(map[string][]string),
componentItems: make(map[string][]string),
Expand All @@ -484,14 +484,83 @@ func TestCollectVolatileConfigItems(t *testing.T) {
},
},
configProvider: &MockConfigProvider{effectiveTime: fixedTime},
expectedItems: &Criteria{
digestItems: make(map[string][]string),
componentItems: make(map[string][]string),
defaultItems: []string{"existing-item"},
},
expectedSuccess: true, // Item is skipped when timestamp is unparseable (fail-closed)
},
{
name: "Both timestamps garbage - item skipped",
items: &Criteria{
digestItems: make(map[string][]string),
componentItems: make(map[string][]string),
defaultItems: []string{"existing-item"},
},
volatileCriteria: []ecc.VolatileCriteria{
{
Value: "garbage-item",
EffectiveOn: "garbage",
EffectiveUntil: "also-garbage",
ImageDigest: "sha256:abc123",
},
},
configProvider: &MockConfigProvider{effectiveTime: fixedTime},
expectedItems: &Criteria{
digestItems: make(map[string][]string),
componentItems: make(map[string][]string),
defaultItems: []string{"existing-item"},
},
expectedSuccess: true,
},
{
name: "Date-only format accepted as fallback",
items: &Criteria{
digestItems: make(map[string][]string),
componentItems: make(map[string][]string),
defaultItems: []string{"existing-item"},
},
volatileCriteria: []ecc.VolatileCriteria{
{
Value: "date-only-item",
EffectiveOn: "2025-08-01",
EffectiveUntil: "2025-08-31",
ImageDigest: "sha256:dateonly",
},
},
configProvider: &MockConfigProvider{effectiveTime: fixedTime},
expectedItems: &Criteria{
digestItems: map[string][]string{
"sha256:def456": {"partial-invalid-item"},
"sha256:dateonly": {"date-only-item"},
},
componentItems: make(map[string][]string),
defaultItems: []string{"existing-item"},
},
expectedSuccess: true,
},
{
name: "EffectiveOn garbage with valid EffectiveUntil - item skipped",
items: &Criteria{
digestItems: make(map[string][]string),
componentItems: make(map[string][]string),
defaultItems: []string{"existing-item"},
},
volatileCriteria: []ecc.VolatileCriteria{
{
Value: "bad-on-item",
EffectiveOn: "garbage",
EffectiveUntil: "2025-08-31T23:59:59Z",
ImageDigest: "sha256:badon",
},
},
configProvider: &MockConfigProvider{effectiveTime: fixedTime},
expectedItems: &Criteria{
digestItems: make(map[string][]string),
componentItems: make(map[string][]string),
defaultItems: []string{"existing-item"},
},
expectedSuccess: true, // Function handles invalid times gracefully
expectedSuccess: true,
},
{
name: "Component names with volatile criteria",
Expand Down
17 changes: 15 additions & 2 deletions internal/policy/equivalence/equivalence.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ import (

ecc "github.com/conforma/crds/api/v1alpha1"
"github.com/pmezard/go-difflib/difflib"
log "github.com/sirupsen/logrus"

"github.com/conforma/cli/internal/timeutil"
)
Comment thread
qodo-for-conforma[bot] marked this conversation as resolved.

// ImageInfo represents information about an image for volatile config matching
Expand Down Expand Up @@ -324,12 +327,22 @@ func (ec *EquivalenceChecker) getActiveVolatileMatchers(v *ecc.VolatileSourceCon

func (ec *EquivalenceChecker) isVolatileMatcherActive(m ecc.VolatileCriteria) bool {
if m.EffectiveOn != "" {
if t, err := time.Parse(time.RFC3339, m.EffectiveOn); err == nil && ec.effectiveTime.Before(t) {
t, err := timeutil.ParseVolatileTime(m.EffectiveOn)
if err != nil {
log.Warnf("skipping volatile matcher %q: %v", m.Value, err)
return false
}
if ec.effectiveTime.Before(t) {
return false
}
}
if m.EffectiveUntil != "" {
if t, err := time.Parse(time.RFC3339, m.EffectiveUntil); err == nil && ec.effectiveTime.After(t) {
t, err := timeutil.ParseVolatileTime(m.EffectiveUntil)
if err != nil {
log.Warnf("skipping volatile matcher %q: %v", m.Value, err)
return false
}
if ec.effectiveTime.After(t) {
return false
}
}
Expand Down
97 changes: 97 additions & 0 deletions internal/policy/equivalence/equivalence_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
//
// SPDX-License-Identifier: Apache-2.0

//go:build unit

package equivalence

import (
Expand Down Expand Up @@ -2043,3 +2045,98 @@ func TestVolatileConfigWithImageInfo(t *testing.T) {
})
}
}

func TestIsVolatileMatcherActive(t *testing.T) {
effectiveTime := time.Date(2025, 8, 18, 12, 0, 0, 0, time.UTC)
checker := NewEquivalenceChecker(effectiveTime, nil)

Comment thread
qodo-for-conforma[bot] marked this conversation as resolved.
tests := []struct {
name string
matcher ecc.VolatileCriteria
expected bool
}{
{
name: "valid timestamps in range returns true",
matcher: ecc.VolatileCriteria{
Value: "some-rule",
EffectiveOn: "2025-08-01T00:00:00Z",
EffectiveUntil: "2025-08-31T23:59:59Z",
},
expected: true,
},
{
name: "empty timestamps returns true (open-ended)",
matcher: ecc.VolatileCriteria{
Value: "open-ended-rule",
},
expected: true,
},
{
name: "garbage EffectiveOn returns false (fail-closed)",
matcher: ecc.VolatileCriteria{
Value: "garbage-on",
EffectiveOn: "not-a-date",
},
expected: false,
},
{
name: "garbage EffectiveUntil returns false (fail-closed)",
matcher: ecc.VolatileCriteria{
Value: "garbage-until",
EffectiveUntil: "also-garbage",
},
expected: false,
},
{
name: "both timestamps garbage returns false",
matcher: ecc.VolatileCriteria{
Value: "both-garbage",
EffectiveOn: "garbage",
EffectiveUntil: "garbage",
},
expected: false,
},
{
name: "date-only format accepted, in range returns true",
matcher: ecc.VolatileCriteria{
Value: "date-only-rule",
EffectiveOn: "2025-08-01",
EffectiveUntil: "2025-08-31",
},
expected: true,
},
{
name: "date-only format, out of range returns false",
matcher: ecc.VolatileCriteria{
Value: "date-only-expired",
EffectiveOn: "2025-07-01",
EffectiveUntil: "2025-07-15",
},
expected: false,
},
{
name: "future EffectiveOn returns false",
matcher: ecc.VolatileCriteria{
Value: "future-rule",
EffectiveOn: "2025-09-01T00:00:00Z",
EffectiveUntil: "2025-09-30T23:59:59Z",
},
expected: false,
},
{
name: "expired EffectiveUntil returns false",
matcher: ecc.VolatileCriteria{
Value: "expired-rule",
EffectiveOn: "2025-07-01T00:00:00Z",
EffectiveUntil: "2025-07-31T23:59:59Z",
},
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, checker.isVolatileMatcherActive(tt.matcher))
})
}
}
37 changes: 37 additions & 0 deletions internal/timeutil/timeutil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright The Conforma Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package timeutil

import (
"fmt"
"time"
)

// Must match policy.DateFormat.
Comment thread
st3penta marked this conversation as resolved.
Comment thread
st3penta marked this conversation as resolved.
const DateFormat = "2006-01-02"
Comment thread
st3penta marked this conversation as resolved.

// ParseVolatileTime tries RFC3339 first, then date-only ("2006-01-02") as a
// fallback, matching the convention used in policy.ParseEffectiveTime.
Comment thread
st3penta marked this conversation as resolved.
func ParseVolatileTime(s string) (time.Time, error) {
Comment thread
st3penta marked this conversation as resolved.
if t, err := time.Parse(time.RFC3339, s); err == nil {
return t, nil
}
if t, err := time.Parse(DateFormat, s); err == nil {
return t, nil
}
return time.Time{}, fmt.Errorf("unable to parse %q as RFC3339 or %s", s, DateFormat)
}
Loading
Loading