diff --git a/apis/projectcontour/v1/detailedconditions.go b/apis/projectcontour/v1/detailedconditions.go index 33bbde17a32..e1ce2d24dbc 100644 --- a/apis/projectcontour/v1/detailedconditions.go +++ b/apis/projectcontour/v1/detailedconditions.go @@ -111,6 +111,11 @@ type DetailedCondition struct { // An empty slice here indicates no warnings. // +optional Warnings []SubCondition `json:"warnings,omitempty"` + // Details contains a slice of relevant detail subconditions for this object. + // + // Subconditions expose detailed information about the object. + // +optional + Details []SubCondition `json:"infos,omitempty"` } const ValidConditionType string = "Valid" diff --git a/apis/projectcontour/v1/helpers.go b/apis/projectcontour/v1/helpers.go index 947bdcdde70..bd36c100d43 100644 --- a/apis/projectcontour/v1/helpers.go +++ b/apis/projectcontour/v1/helpers.go @@ -110,6 +110,19 @@ func (dc *DetailedCondition) AddError(errorType, reason, message string) { }) } +// AddDetail adds an info-level Subcondition to the DetailedCondition. +// If a SubCondition with the given detailType exists, will overwrite the details. +func (dc *DetailedCondition) AddDetail(detailType, reason, message string) { + message = truncateLongMessage(message) + + dc.Details = append(dc.Details, SubCondition{ + Type: detailType, + Status: ConditionTrue, + Message: message, + Reason: reason, + }) +} + // AddErrorf adds an error-level Subcondition to the DetailedCondition, using // fmt.Sprintf on the formatmsg and args params. // If a SubCondition with the given errorType exists, will overwrite the details. @@ -130,6 +143,19 @@ func (dc *DetailedCondition) GetError(errorType string) (SubCondition, bool) { return dc.Errors[i], true } +// GetDetail gets a detail of the given detailType. +// Similar to a hash lookup, will return true in the second value if a match is +// found, and false otherwise. +func (dc *DetailedCondition) GetDetail(detailType string) (SubCondition, bool) { + i := getIndex(detailType, dc.Details) + + if i == -1 { + return SubCondition{}, false + } + + return dc.Details[i], true +} + // AddWarning adds an warning-level Subcondition to the DetailedCondition. // If a SubCondition with the given warnType exists, will overwrite the details. // Note that adding warnings does not update the DetailedCondition Reason or Message. diff --git a/apis/projectcontour/v1/helpers_test.go b/apis/projectcontour/v1/helpers_test.go index 65178326ae3..543f3194567 100644 --- a/apis/projectcontour/v1/helpers_test.go +++ b/apis/projectcontour/v1/helpers_test.go @@ -436,6 +436,169 @@ func TestAddWarningConditions(t *testing.T) { } } +func TestAddDetailConditions(t *testing.T) { + + tests := map[string]struct { + dc *DetailedCondition + subconditions []subConditionDetails + want *DetailedCondition + }{ + "basic detail add": { + dc: &DetailedCondition{}, + subconditions: []subConditionDetails{ + { + condType: "SimpleTest", + reason: "TestReason", + message: "We had a straightforward warning", + }, + }, + want: &DetailedCondition{ + Details: []SubCondition{ + { + Type: "SimpleTest", + Reason: "TestReason", + Message: "We had a straightforward warning", + Status: ConditionTrue, + }, + }, + }, + }, + "multiple reason, multiple type": { + dc: &DetailedCondition{}, + subconditions: []subConditionDetails{ + { + condType: "SimpleTest", + reason: "TestReason", + message: "We had a straightforward warning", + }, + { + condType: "SecondTest", + reason: "TestReason2", + message: "We had an extra straightforward warning", + }, + }, + want: &DetailedCondition{ + Details: []SubCondition{ + { + Type: "SimpleTest", + Reason: "TestReason", + Message: "We had a straightforward warning", + Status: ConditionTrue, + }, + { + Type: "SecondTest", + Reason: "TestReason2", + Message: "We had an extra straightforward warning", + Status: ConditionTrue, + }, + }, + }, + }, + "same reason, multiple type": { + dc: &DetailedCondition{}, + subconditions: []subConditionDetails{ + { + condType: "SimpleTest", + reason: "TestReason", + message: "We had a straightforward warning", + }, + { + condType: "SecondTest", + reason: "TestReason", + message: "We had an extra straightforward warning", + }, + }, + want: &DetailedCondition{ + Details: []SubCondition{ + { + Type: "SimpleTest", + Reason: "TestReason", + Message: "We had a straightforward warning", + Status: ConditionTrue, + }, + { + Type: "SecondTest", + Reason: "TestReason", + Message: "We had an extra straightforward warning", + Status: ConditionTrue, + }, + }, + }, + }, + "same reason, same type": { + dc: &DetailedCondition{}, + subconditions: []subConditionDetails{ + { + condType: "SimpleTest", + reason: "TestReason", + message: "We had a straightforward warning", + }, + { + condType: "SimpleTest", + reason: "TestReason", + message: "We had an extra straightforward warning", + }, + }, + want: &DetailedCondition{ + Details: []SubCondition{ + { + Type: "SimpleTest", + Reason: "TestReason", + Message: "We had a straightforward warning", + Status: ConditionTrue, + }, + { + Type: "SimpleTest", + Reason: "TestReason", + Message: "We had an extra straightforward warning", + Status: ConditionTrue, + }, + }, + }, + }, + "multiple different reason, same type": { + dc: &DetailedCondition{}, + subconditions: []subConditionDetails{ + { + condType: "SimpleTest", + reason: "TestReason", + message: "We had a straightforward warning", + }, + { + condType: "SimpleTest", + reason: "TestReason2", + message: "We had an extra straightforward warning", + }, + }, + want: &DetailedCondition{ + Details: []SubCondition{ + { + Type: "SimpleTest", + Reason: "TestReason", + Message: "We had a straightforward warning", + Status: ConditionTrue, + }, + { + Type: "SimpleTest", + Reason: "TestReason2", + Message: "We had an extra straightforward warning", + Status: ConditionTrue, + }, + }, + }, + }, + } + + for name, tc := range tests { + + for _, cond := range tc.subconditions { + tc.dc.AddDetail(cond.condType, cond.reason, cond.message) + } + + assert.Equalf(t, tc.want, tc.dc, "Add error condition failed in test %s", name) + } +} + func TestGetConditionFor(t *testing.T) { tests := map[string]struct { status HTTPProxyStatus @@ -624,6 +787,47 @@ func TestGetWarning(t *testing.T) { assert.Equal(t, SubCondition{}, emptySubCond) } + +func TestGetDetail(t *testing.T) { + + dcWithErrors := &DetailedCondition{ + Details: []SubCondition{ + { + Type: "SimpleTest1", + Reason: "SimpleReason", + Message: "We had a simple warning 1", + Status: ConditionTrue, + }, + { + Type: "SimpleTest2", + Reason: "SimpleReason", + Message: "We had a simple warning 2", + Status: ConditionTrue, + }, + }, + } + + firstSubCond := SubCondition{ + Type: "SimpleTest1", + Reason: "SimpleReason", + Message: "We had a simple warning 1", + Status: ConditionTrue, + } + + gotSubCond, ok := dcWithErrors.GetDetail("SimpleTest1") + assert.True(t, ok) + assert.Equal(t, firstSubCond, gotSubCond) + + nonExistentCond, ok := dcWithErrors.GetDetail("nonexistent") + assert.False(t, ok) + assert.Equal(t, SubCondition{}, nonExistentCond) + + dcEmpty := &DetailedCondition{} + emptySubCond, ok := dcEmpty.GetDetail("SimpleTest1") + assert.False(t, ok) + assert.Equal(t, SubCondition{}, emptySubCond) +} + func TestTruncateLongMessage(t *testing.T) { shortmessage := "This is a message shorter than the max length" diff --git a/apis/projectcontour/v1/zz_generated.deepcopy.go b/apis/projectcontour/v1/zz_generated.deepcopy.go index 1de68c10414..f6bc0835ecd 100644 --- a/apis/projectcontour/v1/zz_generated.deepcopy.go +++ b/apis/projectcontour/v1/zz_generated.deepcopy.go @@ -152,6 +152,11 @@ func (in *DetailedCondition) DeepCopyInto(out *DetailedCondition) { *out = make([]SubCondition, len(*in)) copy(*out, *in) } + if in.Details != nil { + in, out := &in.Details, &out.Details + *out = make([]SubCondition, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DetailedCondition. diff --git a/examples/contour/01-crds.yaml b/examples/contour/01-crds.yaml index e8b54e14f4f..75e22b2dc0c 100644 --- a/examples/contour/01-crds.yaml +++ b/examples/contour/01-crds.yaml @@ -147,6 +147,40 @@ spec: - type type: object type: array + infos: + description: "Details contains a slice of relevant detail subconditions for this object. \n Subconditions expose detailed information about the object." + items: + description: "SubCondition is a Condition-like type intended for use as a subcondition inside a DetailedCondition. \n It contains a subset of the Condition fields. \n It is intended for warnings and errors, so `type` names should use abnormal-true polarity, that is, they should be of the form \"ErrorPresent: true\". \n The expected lifecycle for these errors is that they should only be present when the error or warning is, and should be removed when they are not relevant." + properties: + message: + description: "Message is a human readable message indicating details about the transition. \n This may be an empty string." + maxLength: 32768 + type: string + reason: + description: "Reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. \n The value should be a CamelCase string. \n This field may not be empty." + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: Status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: "Type of condition in `CamelCase` or in `foo.example.com/CamelCase`. \n This must be in abnormal-true polarity, that is, `ErrorFound` or `controller.io/ErrorFound`. \n The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)" + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - message + - reason + - status + - type + type: object + type: array lastTransitionTime: description: "lastTransitionTime is the last time the condition transitioned from one status to another. \n This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable." format: date-time @@ -978,6 +1012,40 @@ spec: - type type: object type: array + infos: + description: "Details contains a slice of relevant detail subconditions for this object. \n Subconditions expose detailed information about the object." + items: + description: "SubCondition is a Condition-like type intended for use as a subcondition inside a DetailedCondition. \n It contains a subset of the Condition fields. \n It is intended for warnings and errors, so `type` names should use abnormal-true polarity, that is, they should be of the form \"ErrorPresent: true\". \n The expected lifecycle for these errors is that they should only be present when the error or warning is, and should be removed when they are not relevant." + properties: + message: + description: "Message is a human readable message indicating details about the transition. \n This may be an empty string." + maxLength: 32768 + type: string + reason: + description: "Reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. \n The value should be a CamelCase string. \n This field may not be empty." + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: Status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: "Type of condition in `CamelCase` or in `foo.example.com/CamelCase`. \n This must be in abnormal-true polarity, that is, `ErrorFound` or `controller.io/ErrorFound`. \n The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)" + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - message + - reason + - status + - type + type: object + type: array lastTransitionTime: description: "lastTransitionTime is the last time the condition transitioned from one status to another. \n This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable." format: date-time @@ -1188,6 +1256,40 @@ spec: - type type: object type: array + infos: + description: "Details contains a slice of relevant detail subconditions for this object. \n Subconditions expose detailed information about the object." + items: + description: "SubCondition is a Condition-like type intended for use as a subcondition inside a DetailedCondition. \n It contains a subset of the Condition fields. \n It is intended for warnings and errors, so `type` names should use abnormal-true polarity, that is, they should be of the form \"ErrorPresent: true\". \n The expected lifecycle for these errors is that they should only be present when the error or warning is, and should be removed when they are not relevant." + properties: + message: + description: "Message is a human readable message indicating details about the transition. \n This may be an empty string." + maxLength: 32768 + type: string + reason: + description: "Reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. \n The value should be a CamelCase string. \n This field may not be empty." + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: Status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: "Type of condition in `CamelCase` or in `foo.example.com/CamelCase`. \n This must be in abnormal-true polarity, that is, `ErrorFound` or `controller.io/ErrorFound`. \n The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)" + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - message + - reason + - status + - type + type: object + type: array lastTransitionTime: description: "lastTransitionTime is the last time the condition transitioned from one status to another. \n This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable." format: date-time diff --git a/examples/render/contour.yaml b/examples/render/contour.yaml index db13d7ac3c5..6bbfa4d7647 100644 --- a/examples/render/contour.yaml +++ b/examples/render/contour.yaml @@ -268,6 +268,40 @@ spec: - type type: object type: array + infos: + description: "Details contains a slice of relevant detail subconditions for this object. \n Subconditions expose detailed information about the object." + items: + description: "SubCondition is a Condition-like type intended for use as a subcondition inside a DetailedCondition. \n It contains a subset of the Condition fields. \n It is intended for warnings and errors, so `type` names should use abnormal-true polarity, that is, they should be of the form \"ErrorPresent: true\". \n The expected lifecycle for these errors is that they should only be present when the error or warning is, and should be removed when they are not relevant." + properties: + message: + description: "Message is a human readable message indicating details about the transition. \n This may be an empty string." + maxLength: 32768 + type: string + reason: + description: "Reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. \n The value should be a CamelCase string. \n This field may not be empty." + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: Status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: "Type of condition in `CamelCase` or in `foo.example.com/CamelCase`. \n This must be in abnormal-true polarity, that is, `ErrorFound` or `controller.io/ErrorFound`. \n The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)" + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - message + - reason + - status + - type + type: object + type: array lastTransitionTime: description: "lastTransitionTime is the last time the condition transitioned from one status to another. \n This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable." format: date-time @@ -1099,6 +1133,40 @@ spec: - type type: object type: array + infos: + description: "Details contains a slice of relevant detail subconditions for this object. \n Subconditions expose detailed information about the object." + items: + description: "SubCondition is a Condition-like type intended for use as a subcondition inside a DetailedCondition. \n It contains a subset of the Condition fields. \n It is intended for warnings and errors, so `type` names should use abnormal-true polarity, that is, they should be of the form \"ErrorPresent: true\". \n The expected lifecycle for these errors is that they should only be present when the error or warning is, and should be removed when they are not relevant." + properties: + message: + description: "Message is a human readable message indicating details about the transition. \n This may be an empty string." + maxLength: 32768 + type: string + reason: + description: "Reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. \n The value should be a CamelCase string. \n This field may not be empty." + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: Status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: "Type of condition in `CamelCase` or in `foo.example.com/CamelCase`. \n This must be in abnormal-true polarity, that is, `ErrorFound` or `controller.io/ErrorFound`. \n The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)" + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - message + - reason + - status + - type + type: object + type: array lastTransitionTime: description: "lastTransitionTime is the last time the condition transitioned from one status to another. \n This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable." format: date-time @@ -1309,6 +1377,40 @@ spec: - type type: object type: array + infos: + description: "Details contains a slice of relevant detail subconditions for this object. \n Subconditions expose detailed information about the object." + items: + description: "SubCondition is a Condition-like type intended for use as a subcondition inside a DetailedCondition. \n It contains a subset of the Condition fields. \n It is intended for warnings and errors, so `type` names should use abnormal-true polarity, that is, they should be of the form \"ErrorPresent: true\". \n The expected lifecycle for these errors is that they should only be present when the error or warning is, and should be removed when they are not relevant." + properties: + message: + description: "Message is a human readable message indicating details about the transition. \n This may be an empty string." + maxLength: 32768 + type: string + reason: + description: "Reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. \n The value should be a CamelCase string. \n This field may not be empty." + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: Status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: "Type of condition in `CamelCase` or in `foo.example.com/CamelCase`. \n This must be in abnormal-true polarity, that is, `ErrorFound` or `controller.io/ErrorFound`. \n The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)" + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - message + - reason + - status + - type + type: object + type: array lastTransitionTime: description: "lastTransitionTime is the last time the condition transitioned from one status to another. \n This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable." format: date-time