Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
22 changes: 19 additions & 3 deletions api/v1alpha1/mustgather_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

// MustGatherSpec defines the desired state of MustGather
// +kubebuilder:validation:XValidation:rule="!(has(self.gatherSpec) && (size(self.gatherSpec.command) > 0 || size(self.gatherSpec.args) > 0)) || has(self.imageStreamRef)",message="command and args in gatherSpec can only be set when imageStreamRef is specified"
// +kubebuilder:validation:XValidation:rule="!(has(self.gatherSpec) && ((has(self.gatherSpec.command) && size(self.gatherSpec.command) > 0) || (has(self.gatherSpec.args) && size(self.gatherSpec.args) > 0))) || has(self.imageStreamRef)",message="command and args in gatherSpec can only be set when imageStreamRef is specified"
type MustGatherSpec struct {
// the service account to use to run the must gather job pod, defaults to default
// +kubebuilder:validation:Optional
Expand All @@ -37,7 +37,8 @@ type MustGatherSpec struct {
// +kubebuilder:validation:Optional
ImageStreamRef *ImageStreamTagRef `json:"imageStreamRef,omitempty"`

// GatherSpec allows overriding the command and/or arguments for the custom must-gather image.
// GatherSpec allows overriding the command and/or arguments for the custom must-gather image
// and configures time-based collection filters.
// This field is ignored if ImageStreamRef is not specified.
// +kubebuilder:validation:Optional
GatherSpec *GatherSpec `json:"gatherSpec,omitempty"`
Comment on lines +40 to 44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Root cause of the misleading description — update the Go doc comment.

Same issue as in the CRD: line 42 states "This field is ignored if ImageStreamRef is not specified," which is no longer accurate since since/sinceTime are usable without imageStreamRef. The CRD is generated from these markers, so the fix belongs here.

Proposed fix
-	// GatherSpec allows overriding the command and/or arguments for the custom must-gather image
-	// and configures time-based collection filters.
-	// This field is ignored if ImageStreamRef is not specified.
+	// GatherSpec allows overriding the command and/or arguments for the custom must-gather image
+	// and configures time-based collection filters.
+	// The command and args fields are only honored when ImageStreamRef is specified.
+	// Time-based filters (since, sinceTime) apply regardless of ImageStreamRef.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v1alpha1/mustgather_types.go` around lines 40 - 44, Update the Go doc
comment above the GatherSpec field in mustgather_types.go: remove the inaccurate
sentence "This field is ignored if ImageStreamRef is not specified" and replace
it with a precise description noting that while command/args from GatherSpec are
only applied when ImageStreamRef is provided, time-based filters (since and
sinceTime) are applicable even without ImageStreamRef; reference the GatherSpec
field, ImageStreamRef, and the since/sinceTime filters in the comment so the
generated CRD correctly documents the behavior.

Expand Down Expand Up @@ -66,7 +67,8 @@ type MustGatherSpec struct {
Storage *Storage `json:"storage,omitempty"`
}

// GatherSpec allows specifying the execution details for a must-gather run.
// GatherSpec allows specifying the execution details for a must-gather run and the collection behavior.
// +kubebuilder:validation:XValidation:rule="!(has(self.since) && has(self.sinceTime))",message="only one of since or sinceTime may be specified"
type GatherSpec struct {
// +kubebuilder:validation:Optional
// Audit specifies whether to collect audit logs. This is translated to a signal
Expand All @@ -87,6 +89,20 @@ type GatherSpec struct {
// +kubebuilder:validation:MaxItems=256
// +kubebuilder:validation:Items:MaxLength=256
Args []string `json:"args,omitempty"`

// Since only returns logs newer than a relative duration like "2h" or "30m".
// This is passed to the must-gather script to filter log collection.
// Only one of since or sinceTime may be specified.
// +kubebuilder:validation:Optional
// +kubebuilder:validation:Format=duration
Since *metav1.Duration `json:"since,omitempty"`

// SinceTime only returns logs after a specific date/time (RFC3339 format).
// This is passed to the must-gather script to filter log collection.
// Only one of since or sinceTime may be specified.
// +kubebuilder:validation:Optional
// +kubebuilder:validation:Format=date-time
SinceTime *metav1.Time `json:"sinceTime,omitempty"`
}

// ImageStreamTagRef provides a structured reference to a specific tag within an ImageStream.
Expand Down
22 changes: 21 additions & 1 deletion controllers/mustgather/mustgather_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import (
goerror "errors"
"fmt"
"os"
"time"

"github.com/go-logr/logr"
configv1 "github.com/openshift/api/config/v1"
imagev1 "github.com/openshift/api/image/v1"
mustgatherv1alpha1 "github.com/openshift/must-gather-operator/api/v1alpha1"
"github.com/openshift/must-gather-operator/pkg/localmetrics"
Expand Down Expand Up @@ -411,7 +413,25 @@ func (r *MustGatherReconciler) getJobFromInstance(ctx context.Context, instance
return nil, err
}

return getJobTemplate(image, operatorImage, *instance, r.TrustedCAConfigMap), nil
// Best-effort fetch of cluster creation time (used to clamp since/sinceTime filters).
// Errors here must NOT block must-gather execution.
var clusterCreationTime *time.Time
if t, err := r.getClusterCreationTime(ctx); err != nil {
log.V(2).Info("unable to determine cluster creation time; since filters will not be clamped", "err", err)
} else {
clusterCreationTime = t
}

return getJobTemplate(image, operatorImage, *instance, r.TrustedCAConfigMap, clusterCreationTime), nil
}

func (r *MustGatherReconciler) getClusterCreationTime(ctx context.Context) (*time.Time, error) {
cv := &configv1.ClusterVersion{}
if err := r.GetClient().Get(ctx, types.NamespacedName{Name: "version"}, cv); err != nil {
return nil, err
}
t := cv.CreationTimestamp.Time
return &t, nil
}

func (r *MustGatherReconciler) getMustGatherImage(ctx context.Context, instance *mustgatherv1alpha1.MustGather) (string, error) {
Expand Down
71 changes: 68 additions & 3 deletions controllers/mustgather/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const (
gatherCommand = "timeout %v bash -x -c -- '/usr/bin/%v' 2>&1 | tee /must-gather/must-gather.log\n\nstatus=$?\nif [[ $status -eq 124 || $status -eq 137 ]]; then\n echo \"Gather timed out.\"\n exit 0\nfi | tee -a /must-gather/must-gather.log"
gatherContainerName = "gather"

// Environment variables for time-based log filtering
gatherEnvSince = "MUST_GATHER_SINCE"
gatherEnvSinceTime = "MUST_GATHER_SINCE_TIME"

backoffLimit = 3
uploadContainerName = "upload"
uploadEnvUsername = "username"
Expand All @@ -49,7 +53,18 @@ const (
knownHostsFile = "/tmp/must-gather-operator/.ssh/known_hosts"
)

func getJobTemplate(image string, operatorImage string, mustGather v1alpha1.MustGather, trustedCAConfigMapName string) *batchv1.Job {
// timeNow exists to allow deterministic unit testing of time-based behavior.
var timeNow = time.Now

// GatherTimeFilter holds the time-based filtering options for log collection
type GatherTimeFilter struct {
// Since is a relative duration (e.g., "2h", "30m")
Since time.Duration
// SinceTime is an absolute timestamp
SinceTime *time.Time
}

func getJobTemplate(image string, operatorImage string, mustGather v1alpha1.MustGather, trustedCAConfigMapName string, clusterCreationTime *time.Time) *batchv1.Job {
job := initializeJobTemplate(mustGather.Name, mustGather.Namespace, mustGather.Spec.ServiceAccountName, mustGather.Spec.Storage, trustedCAConfigMapName)

var httpProxy, httpsProxy, noProxy string
Expand Down Expand Up @@ -78,15 +93,27 @@ func getJobTemplate(image string, operatorImage string, mustGather v1alpha1.Must
timeout = mustGather.Spec.MustGatherTimeout.Duration
}

// Build time filter from spec
var timeFilter *GatherTimeFilter
var command, args []string
if mustGather.Spec.GatherSpec != nil {
command = mustGather.Spec.GatherSpec.Command
args = mustGather.Spec.GatherSpec.Args
if mustGather.Spec.GatherSpec.Since != nil || mustGather.Spec.GatherSpec.SinceTime != nil {
timeFilter = &GatherTimeFilter{}
if mustGather.Spec.GatherSpec.Since != nil {
timeFilter.Since = mustGather.Spec.GatherSpec.Since.Duration
}
if mustGather.Spec.GatherSpec.SinceTime != nil {
t := mustGather.Spec.GatherSpec.SinceTime.Time
timeFilter.SinceTime = &t
}
}
}

job.Spec.Template.Spec.Containers = append(
job.Spec.Template.Spec.Containers,
getGatherContainer(image, audit, timeout, mustGather.Spec.Storage, trustedCAConfigMapName, command, args),
getGatherContainer(image, audit, timeout, mustGather.Spec.Storage, trustedCAConfigMapName, timeFilter, clusterCreationTime, command, args),
)

// Add the upload container only if the upload target is specified
Expand Down Expand Up @@ -192,7 +219,7 @@ func initializeJobTemplate(name string, namespace string, serviceAccountRef stri
}
}

func getGatherContainer(image string, audit bool, timeout time.Duration, storage *v1alpha1.Storage, trustedCAConfigMapName string, command []string, args []string) corev1.Container {
func getGatherContainer(image string, audit bool, timeout time.Duration, storage *v1alpha1.Storage, trustedCAConfigMapName string, timeFilter *GatherTimeFilter, clusterCreationTime *time.Time, command []string, args []string) corev1.Container {
var commandBinary string
if audit {
commandBinary = gatherCommandBinaryAudit
Expand Down Expand Up @@ -240,6 +267,44 @@ func getGatherContainer(image string, audit bool, timeout time.Duration, storage
container.Args = args
}

// Add time filter environment variables if specified
if timeFilter != nil {
// Clamp the time filter so it never precedes cluster creation time.
// - For Since (duration): ensure (now - since) >= clusterCreationTime by reducing Since to clusterAge.
// - For SinceTime (absolute): ensure sinceTime >= clusterCreationTime by bumping it up.
effectiveSince := timeFilter.Since
effectiveSinceTime := timeFilter.SinceTime
if clusterCreationTime != nil && !clusterCreationTime.IsZero() {
now := timeNow()
if effectiveSince > 0 {
clusterAge := now.Sub(*clusterCreationTime)
if clusterAge < 0 {
clusterAge = 0
}
if effectiveSince > clusterAge {
effectiveSince = clusterAge
}
}
if effectiveSinceTime != nil && effectiveSinceTime.Before(*clusterCreationTime) {
t := *clusterCreationTime
effectiveSinceTime = &t
}
}

if effectiveSince > 0 {
container.Env = append(container.Env, corev1.EnvVar{
Name: gatherEnvSince,
Value: effectiveSince.String(),
})
}
if effectiveSinceTime != nil {
container.Env = append(container.Env, corev1.EnvVar{
Name: gatherEnvSinceTime,
Value: effectiveSinceTime.Format(time.RFC3339),
})
}
}

return container
}

Expand Down
Loading