diff --git a/api/v1alpha1/freight_helpers.go b/api/v1alpha1/freight_helpers.go index a3707dbf6..6d8de7f5b 100644 --- a/api/v1alpha1/freight_helpers.go +++ b/api/v1alpha1/freight_helpers.go @@ -3,7 +3,9 @@ package v1alpha1 import ( "context" "fmt" + "time" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -85,26 +87,128 @@ func GetFreightByAlias( return &freightList.Items[0], nil } -// IsFreightAvailable answers whether the specified Freight is available to the -// specified Stage. -func IsFreightAvailable(stage *Stage, freight *Freight) bool { - if stage == nil || freight == nil || stage.Namespace != freight.Namespace { - return false +// ListFreightByCurrentStage returns a list of Freight resources that think +// they're currently in use by the Stage specified. +func ListFreightByCurrentStage( + ctx context.Context, + c client.Client, + stage *Stage, +) ([]Freight, error) { + freightList := FreightList{} + if err := c.List( + ctx, + &freightList, + client.InNamespace(stage.Namespace), + client.MatchingFields{"currentlyIn": stage.Name}, + ); err != nil { + return nil, fmt.Errorf( + "error listing Freight in namespace %q with current stage %q: %w", + stage.Namespace, + stage.Name, + err, + ) + } + return freightList.Items, nil +} + +// IsCurrentlyIn returns whether the Freight is currently in the specified +// Stage. +func (f *Freight) IsCurrentlyIn(stage string) bool { + // NB: This method exists for convenience. It doesn't require the caller to + // know anything about the Freight status' internal data structure. + _, in := f.Status.CurrentlyIn[stage] + return in +} + +// IsVerifiedIn returns whether the Freight has been verified in the specified +// Stage. +func (f *Freight) IsVerifiedIn(stage string) bool { + // NB: This method exists for convenience. It doesn't require the caller to + // know anything about the Freight status' internal data structure. + _, verified := f.Status.VerifiedIn[stage] + return verified +} + +// IsApprovedFor returns whether the Freight has been approved for the specified +// Stage. +func (f *Freight) IsApprovedFor(stage string) bool { + // NB: This method exists for convenience. It doesn't require the caller to + // know anything about the Freight status' internal data structure. + _, approved := f.Status.ApprovedFor[stage] + return approved +} + +// GetLongestSoak returns the longest soak time for the Freight in the specified +// Stage if it's been verified in that Stage. If it has not, zero will be +// returned instead. If the Freight is currently in use by the specified Stage, +// the current soak time is calculated and compared to the longest completed +// soak time on record. +func (f *Freight) GetLongestSoak(stage string) time.Duration { + if _, verified := f.Status.VerifiedIn[stage]; !verified { + return 0 } - if _, approved := freight.Status.ApprovedFor[stage.Name]; approved { - return true + var longestCompleted time.Duration + if record, isVerified := f.Status.VerifiedIn[stage]; isVerified && record.LongestCompletedSoak != nil { + longestCompleted = record.LongestCompletedSoak.Duration } - for _, req := range stage.Spec.RequestedFreight { - if freight.Origin.Equals(&req.Origin) { - if req.Sources.Direct { - return true - } - for _, source := range req.Sources.Stages { - if _, verified := freight.Status.VerifiedIn[source]; verified { - return true + var current time.Duration + if record, isCurrent := f.Status.CurrentlyIn[stage]; isCurrent { + current = time.Since(record.Since.Time) + } + return time.Duration(max(longestCompleted.Nanoseconds(), current.Nanoseconds())) +} + +// AddCurrentStage updates the Freight status to reflect that the Freight is +// currently in the specified Stage. +func (f *FreightStatus) AddCurrentStage(stage string, since time.Time) { + if _, alreadyIn := f.CurrentlyIn[stage]; !alreadyIn { + if f.CurrentlyIn == nil { + f.CurrentlyIn = make(map[string]CurrentStage) + } + f.CurrentlyIn[stage] = CurrentStage{ + Since: &metav1.Time{Time: since}, + } + } +} + +// RemoveCurrentStage updates the Freight status to reflect that the Freight is +// no longer in the specified Stage. If the Freight was verified in the +// specified Stage, the longest completed soak time will be updated if +// necessary. +func (f *FreightStatus) RemoveCurrentStage(stage string) { + if record, in := f.CurrentlyIn[stage]; in { + if _, verified := f.VerifiedIn[stage]; verified { + soak := time.Since(record.Since.Time) + if soak > f.VerifiedIn[stage].LongestCompletedSoak.Duration { + f.VerifiedIn[stage] = VerifiedStage{ + LongestCompletedSoak: &metav1.Duration{Duration: soak}, } } } + delete(f.CurrentlyIn, stage) + } +} + +// AddVerifiedStage updates the Freight status to reflect that the Freight has +// been verified in the specified Stage. +func (f *FreightStatus) AddVerifiedStage(stage string, verifiedAt time.Time) { + if _, verified := f.VerifiedIn[stage]; !verified { + record := VerifiedStage{VerifiedAt: &metav1.Time{Time: verifiedAt}} + if f.VerifiedIn == nil { + f.VerifiedIn = map[string]VerifiedStage{stage: record} + } + f.VerifiedIn[stage] = record + } +} + +// AddApprovedStage updates the Freight status to reflect that the Freight has +// been approved for the specified Stage. +func (f *FreightStatus) AddApprovedStage(stage string, approvedAt time.Time) { + if _, approved := f.ApprovedFor[stage]; !approved { + record := ApprovedStage{ApprovedAt: &metav1.Time{Time: approvedAt}} + if f.ApprovedFor == nil { + f.ApprovedFor = map[string]ApprovedStage{stage: record} + } + f.ApprovedFor[stage] = record } - return false } diff --git a/api/v1alpha1/freight_helpers_test.go b/api/v1alpha1/freight_helpers_test.go index 5d29b2008..ac4e51804 100644 --- a/api/v1alpha1/freight_helpers_test.go +++ b/api/v1alpha1/freight_helpers_test.go @@ -2,7 +2,9 @@ package v1alpha1 import ( "context" + "errors" "testing" + "time" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -10,8 +12,26 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" ) +// TODO(krancour): If we move our actual indexers to this package, we can use +// them here instead of duplicating them for the sake of avoiding an import +// cycle. +const freightByCurrentStagesField = "currentlyIn" + +func freightByCurrentStagesIndexer(obj client.Object) []string { + freight, ok := obj.(*Freight) + if !ok { + return nil + } + currentStages := make([]string, 0, len(freight.Status.CurrentlyIn)) + for stage := range freight.Status.CurrentlyIn { + currentStages = append(currentStages, stage) + } + return currentStages +} + func TestGetFreight(t *testing.T) { scheme := k8sruntime.NewScheme() require.NoError(t, SchemeBuilder.AddToScheme(scheme)) @@ -115,165 +135,312 @@ func TestGetFreightByAlias(t *testing.T) { } } -func TestIsFreightAvailable(t *testing.T) { - const testNamespace = "fake-namespace" - const testWarehouse = "fake-warehouse" - const testStage = "fake-stage" +func TestListFreightByCurrentStage(t *testing.T) { + const testProject = "fake-project" + testStage := &Stage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-stage", + }, + } testCases := []struct { - name string - stage *Stage - Freight *Freight - expected bool + name string + objects []client.Object + interceptor interceptor.Funcs + assertions func(*testing.T, []Freight, error) }{ { - name: "stage is nil", - expected: false, - }, - { - name: "freight is nil", - expected: false, - }, - { - name: "stage and freight are in different namespaces", - stage: &Stage{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, + name: "error listing Freight", + interceptor: interceptor.Funcs{ + List: func( + context.Context, + client.WithWatch, + client.ObjectList, + ...client.ListOption, + ) error { + return errors.New("something went wrong") }, }, - Freight: &Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "wrong-namespace", - }, + assertions: func(t *testing.T, freight []Freight, err error) { + require.ErrorContains(t, err, "error listing Freight") + require.ErrorContains(t, err, "something went wrong") + require.Nil(t, freight) }, - expected: false, }, { - name: "freight is approved for stage", - stage: &Stage{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: testStage, - }, - }, - Freight: &Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: testStage, + name: "success", + objects: []client.Object{ + &Freight{ // This should be returned + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight-1", + }, + Status: FreightStatus{ + CurrentlyIn: map[string]CurrentStage{testStage.Name: {}}, + }, }, - Status: FreightStatus{ - ApprovedFor: map[string]ApprovedStage{ - testStage: {}, + &Freight{ // This should NOT be returned + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight-2", + }, + Status: FreightStatus{ + CurrentlyIn: map[string]CurrentStage{"wrong-stage": {}}, }, }, }, - expected: true, + assertions: func(t *testing.T, freight []Freight, err error) { + require.NoError(t, err) + require.Len(t, freight, 1) + require.Equal(t, testProject, freight[0].Namespace) + require.Equal(t, "fake-freight-1", freight[0].Name) + }, }, + } + + testScheme := k8sruntime.NewScheme() + err := AddToScheme(testScheme) + require.NoError(t, err) + + for _, testCase := range testCases { + c := fake.NewClientBuilder().WithScheme(testScheme). + WithScheme(testScheme). + WithIndex(&Freight{}, freightByCurrentStagesField, freightByCurrentStagesIndexer). + WithObjects(testCase.objects...). + WithInterceptorFuncs(testCase.interceptor). + Build() + + t.Run(testCase.name, func(t *testing.T) { + freight, err := ListFreightByCurrentStage(context.Background(), c, testStage) + testCase.assertions(t, freight, err) + }) + } +} + +func TestFreight_IsCurrentlyIn(t *testing.T) { + const testStage = "fake-stage" + freight := &Freight{} + require.False(t, freight.IsCurrentlyIn(testStage)) + freight.Status.CurrentlyIn = map[string]CurrentStage{testStage: {}} + require.True(t, freight.IsCurrentlyIn(testStage)) +} + +func TestFreight_IsVerifiedIn(t *testing.T) { + const testStage = "fake-stage" + freight := &Freight{} + require.False(t, freight.IsVerifiedIn(testStage)) + freight.Status.VerifiedIn = map[string]VerifiedStage{testStage: {}} + require.True(t, freight.IsVerifiedIn(testStage)) +} + +func TestFreight_IsApprovedFor(t *testing.T) { + const testStage = "fake-stage" + freight := &Freight{} + require.False(t, freight.IsApprovedFor(testStage)) + freight.Status.ApprovedFor = map[string]ApprovedStage{testStage: {}} + require.True(t, freight.IsApprovedFor(testStage)) +} + +func TestFreight_GetLongestSoak(t *testing.T) { + testStage := "fake-stage" + testCases := []struct { + name string + status FreightStatus + assertions func(t *testing.T, status FreightStatus, longestSoak time.Duration) + }{ { - name: "stage accepts freight direct from origin", - stage: &Stage{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - }, - Spec: StageSpec{ - RequestedFreight: []FreightRequest{{ - Origin: FreightOrigin{ - Kind: FreightOriginKindWarehouse, - Name: testWarehouse, - }, - Sources: FreightSources{ - Direct: true, - }, - }}, - }, + name: "Freight is not currently in the Stage and was never verified there", + assertions: func(t *testing.T, _ FreightStatus, longestSoak time.Duration) { + require.Zero(t, longestSoak) }, - Freight: &Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - }, - Origin: FreightOrigin{ - Kind: FreightOriginKindWarehouse, - Name: testWarehouse, + }, + { + name: "Freight is not currently in the Stage but was verified there", + status: FreightStatus{ + VerifiedIn: map[string]VerifiedStage{ + testStage: {LongestCompletedSoak: &metav1.Duration{Duration: time.Hour}}, }, }, - expected: true, + assertions: func(t *testing.T, _ FreightStatus, longestSoak time.Duration) { + require.Equal(t, time.Hour, longestSoak) + }, }, { - name: "freight is verified in an upstream stage", - stage: &Stage{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - }, - Spec: StageSpec{ - RequestedFreight: []FreightRequest{{ - Origin: FreightOrigin{ - Kind: FreightOriginKindWarehouse, - Name: testWarehouse, - }, - Sources: FreightSources{ - Stages: []string{"upstream-stage"}, - }, - }}, + name: "Freight is currently in the Stage but was never verified there", + status: FreightStatus{ + CurrentlyIn: map[string]CurrentStage{ + testStage: {Since: &metav1.Time{Time: time.Now().Add(-time.Hour)}}, }, }, - Freight: &Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - }, - Origin: FreightOrigin{ - Kind: FreightOriginKindWarehouse, - Name: testWarehouse, - }, - Status: FreightStatus{ - VerifiedIn: map[string]VerifiedStage{ - "upstream-stage": {}, - }, - }, + assertions: func(t *testing.T, _ FreightStatus, longestSoak time.Duration) { + require.Zero(t, longestSoak) }, - expected: true, }, { - name: "freight from origin not requested", - stage: &Stage{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, + name: "Freight is currently in the Stage and has been verified there; current soak is longer", + status: FreightStatus{ + CurrentlyIn: map[string]CurrentStage{ + testStage: {Since: &metav1.Time{Time: time.Now().Add(-2 * time.Hour)}}, }, - Spec: StageSpec{ - RequestedFreight: []FreightRequest{{ - Origin: FreightOrigin{ - Kind: FreightOriginKindWarehouse, - Name: testWarehouse, - }, - Sources: FreightSources{ - Stages: []string{"upstream-stage"}, - }, - }}, + VerifiedIn: map[string]VerifiedStage{ + testStage: {LongestCompletedSoak: &metav1.Duration{Duration: time.Hour}}, }, }, - Freight: &Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - }, - Origin: FreightOrigin{ - Kind: FreightOriginKindWarehouse, - Name: "wrong-warehouse", + assertions: func(t *testing.T, _ FreightStatus, longestSoak time.Duration) { + // Expect these to be equal within a second. TODO(krancour): There's probably a + // more elegant way to do this, but I consider good enough. + require.GreaterOrEqual(t, longestSoak, 2*time.Hour) + require.LessOrEqual(t, longestSoak, 2*time.Hour+time.Second) + }, + }, + { + name: "Freight is currently in the Stage and has been verified there; a previous soak was longer", + status: FreightStatus{ + CurrentlyIn: map[string]CurrentStage{ + testStage: {Since: &metav1.Time{Time: time.Now().Add(-time.Hour)}}, }, - Status: FreightStatus{ - VerifiedIn: map[string]VerifiedStage{ - "upstream-stage": {}, - }, + VerifiedIn: map[string]VerifiedStage{ + testStage: {LongestCompletedSoak: &metav1.Duration{Duration: 2 * time.Hour}}, }, }, - expected: false, + assertions: func(t *testing.T, _ FreightStatus, longestSoak time.Duration) { + // Expect these to be equal within a second. TODO(krancour): There's probably a + // more elegant way to do this, but I consider good enough. + require.GreaterOrEqual(t, longestSoak, 2*time.Hour) + require.LessOrEqual(t, longestSoak, 2*time.Hour+time.Second) + }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - require.Equal( - t, - testCase.expected, - IsFreightAvailable(testCase.stage, testCase.Freight), - ) + freight := &Freight{ + Status: testCase.status, + } + testCase.assertions(t, freight.Status, freight.GetLongestSoak(testStage)) }) } } + +func TestFreightStatus_AddCurrentStage(t *testing.T) { + const testStage = "fake-stage" + now := time.Now() + t.Run("already in current", func(t *testing.T) { + oldTime := now.Add(-time.Hour) + newTime := now + status := FreightStatus{ + CurrentlyIn: map[string]CurrentStage{ + testStage: {Since: &metav1.Time{Time: oldTime}}, + }, + } + status.AddCurrentStage(testStage, newTime) + record, in := status.CurrentlyIn[testStage] + require.True(t, in) + require.Equal(t, oldTime, record.Since.Time) + }) + t.Run("not already in current", func(t *testing.T) { + status := FreightStatus{} + status.AddCurrentStage(testStage, now) + require.NotNil(t, status.CurrentlyIn) + record, in := status.CurrentlyIn[testStage] + require.True(t, in) + require.Equal(t, now, record.Since.Time) + }) +} + +func TestFreightStatus_RemoveCurrentStage(t *testing.T) { + const testStage = "fake-stage" + t.Run("not verified", func(t *testing.T) { + status := FreightStatus{ + CurrentlyIn: map[string]CurrentStage{}, + } + status.RemoveCurrentStage(testStage) + require.NotContains(t, status.CurrentlyIn, testStage) + }) + t.Run("verified; old soak is longer", func(t *testing.T) { + status := FreightStatus{ + CurrentlyIn: map[string]CurrentStage{ + testStage: {Since: &metav1.Time{Time: time.Now().Add(-time.Hour)}}, + }, + VerifiedIn: map[string]VerifiedStage{ + testStage: {LongestCompletedSoak: &metav1.Duration{Duration: 2 * time.Hour}}, + }, + } + status.RemoveCurrentStage(testStage) + require.NotContains(t, status.CurrentlyIn, testStage) + record, verified := status.VerifiedIn[testStage] + require.True(t, verified) + require.Equal(t, 2*time.Hour, record.LongestCompletedSoak.Duration) + }) + t.Run("verified; new soak is longer", func(t *testing.T) { + status := FreightStatus{ + CurrentlyIn: map[string]CurrentStage{ + testStage: {Since: &metav1.Time{Time: time.Now().Add(-2 * time.Hour)}}, + }, + VerifiedIn: map[string]VerifiedStage{ + testStage: {LongestCompletedSoak: &metav1.Duration{Duration: time.Hour}}, + }, + } + status.RemoveCurrentStage(testStage) + require.NotContains(t, status.CurrentlyIn, testStage) + record, verified := status.VerifiedIn[testStage] + require.True(t, verified) + // Expect these to be equal within a second. TODO(krancour): There's probably a + // more elegant way to do this, but I consider good enough. + require.GreaterOrEqual(t, record.LongestCompletedSoak.Duration, 2*time.Hour) + require.LessOrEqual(t, record.LongestCompletedSoak.Duration, 2*time.Hour+time.Second) + }) +} + +func TestFreightStatus_AddVerifiedStage(t *testing.T) { + const testStage = "fake-stage" + now := time.Now() + t.Run("already verified", func(t *testing.T) { + oldTime := now.Add(-time.Hour) + newTime := now + status := FreightStatus{ + VerifiedIn: map[string]VerifiedStage{ + testStage: {VerifiedAt: &metav1.Time{Time: oldTime}}, + }, + } + status.AddVerifiedStage(testStage, newTime) + record, verified := status.VerifiedIn[testStage] + require.True(t, verified) + require.Equal(t, oldTime, record.VerifiedAt.Time) + }) + t.Run("not already verified", func(t *testing.T) { + status := FreightStatus{} + testTime := time.Now() + status.AddVerifiedStage(testStage, testTime) + require.NotNil(t, status.VerifiedIn) + record, verified := status.VerifiedIn[testStage] + require.True(t, verified) + require.Equal(t, testTime, record.VerifiedAt.Time) + }) +} + +func TestFreightStatus_AddApprovedStage(t *testing.T) { + const testStage = "fake-stage" + now := time.Now() + t.Run("already approved", func(t *testing.T) { + oldTime := now.Add(-time.Hour) + newTime := now + status := FreightStatus{ + ApprovedFor: map[string]ApprovedStage{ + testStage: {ApprovedAt: &metav1.Time{Time: oldTime}}, + }, + } + status.AddApprovedStage(testStage, newTime) + record, approved := status.ApprovedFor[testStage] + require.True(t, approved) + require.Equal(t, oldTime, record.ApprovedAt.Time) + }) + t.Run("not already approved", func(t *testing.T) { + status := FreightStatus{} + status.AddApprovedStage(testStage, now) + require.NotNil(t, status.ApprovedFor) + record, approved := status.ApprovedFor[testStage] + require.True(t, approved) + require.Equal(t, now, record.ApprovedAt.Time) + }) +} diff --git a/api/v1alpha1/freight_types.go b/api/v1alpha1/freight_types.go index 2f3c6bf1c..e1bc5e3fe 100644 --- a/api/v1alpha1/freight_types.go +++ b/api/v1alpha1/freight_types.go @@ -162,6 +162,8 @@ func (g *GitCommit) Equals(rhs *GitCommit) bool { // FreightStatus describes a piece of Freight's most recently observed state. type FreightStatus struct { + // CurrentlyIn describes the Stages in which this Freight is currently in use. + CurrentlyIn map[string]CurrentStage `json:"currentlyIn,omitempty" protobuf:"bytes,3,rep,name=currentlyIn" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // VerifiedIn describes the Stages in which this Freight has been verified // through promotion and subsequent health checks. VerifiedIn map[string]VerifiedStage `json:"verifiedIn,omitempty" protobuf:"bytes,1,rep,name=verifiedIn" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` @@ -172,10 +174,24 @@ type FreightStatus struct { ApprovedFor map[string]ApprovedStage `json:"approvedFor,omitempty" protobuf:"bytes,2,rep,name=approvedFor" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` } +// CurrentStage reflects a Stage's current use of Freight. +type CurrentStage struct { + // Since is the time at which the Stage most recently started using the + // Freight. This can be used to calculate how long the Freight has been in use + // by the Stage. + Since *metav1.Time `json:"since,omitempty" protobuf:"bytes,1,opt,name=since"` +} + // VerifiedStage describes a Stage in which Freight has been verified. type VerifiedStage struct { // VerifiedAt is the time at which the Freight was verified in the Stage. VerifiedAt *metav1.Time `json:"verifiedAt,omitempty" protobuf:"bytes,1,opt,name=verifiedAt"` + // LongestCompletedSoak represents the longest definite time interval wherein + // the Freight was in CONTINUOUS use by the Stage. This value is updated as + // Freight EXITS the Stage. If the Freight is currently in use by the Stage, + // the time elapsed since the Freight ENTERED the Stage is its current soak + // time, which may exceed the value of this field. + LongestCompletedSoak *metav1.Duration `json:"longestSoak,omitempty" protobuf:"bytes,2,opt,name=longestSoak"` } // ApprovedStage describes a Stage for which Freight has been (manually) diff --git a/api/v1alpha1/generated.pb.go b/api/v1alpha1/generated.pb.go index 3b9247777..ab5ab173a 100644 --- a/api/v1alpha1/generated.pb.go +++ b/api/v1alpha1/generated.pb.go @@ -338,10 +338,38 @@ func (m *ChartSubscription) XXX_DiscardUnknown() { var xxx_messageInfo_ChartSubscription proto.InternalMessageInfo +func (m *CurrentStage) Reset() { *m = CurrentStage{} } +func (*CurrentStage) ProtoMessage() {} +func (*CurrentStage) Descriptor() ([]byte, []int) { + return fileDescriptor_e26b7f7bbc391025, []int{11} +} +func (m *CurrentStage) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *CurrentStage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *CurrentStage) XXX_Merge(src proto.Message) { + xxx_messageInfo_CurrentStage.Merge(m, src) +} +func (m *CurrentStage) XXX_Size() int { + return m.Size() +} +func (m *CurrentStage) XXX_DiscardUnknown() { + xxx_messageInfo_CurrentStage.DiscardUnknown(m) +} + +var xxx_messageInfo_CurrentStage proto.InternalMessageInfo + func (m *DiscoveredArtifacts) Reset() { *m = DiscoveredArtifacts{} } func (*DiscoveredArtifacts) ProtoMessage() {} func (*DiscoveredArtifacts) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{11} + return fileDescriptor_e26b7f7bbc391025, []int{12} } func (m *DiscoveredArtifacts) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -369,7 +397,7 @@ var xxx_messageInfo_DiscoveredArtifacts proto.InternalMessageInfo func (m *DiscoveredCommit) Reset() { *m = DiscoveredCommit{} } func (*DiscoveredCommit) ProtoMessage() {} func (*DiscoveredCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{12} + return fileDescriptor_e26b7f7bbc391025, []int{13} } func (m *DiscoveredCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -397,7 +425,7 @@ var xxx_messageInfo_DiscoveredCommit proto.InternalMessageInfo func (m *DiscoveredImageReference) Reset() { *m = DiscoveredImageReference{} } func (*DiscoveredImageReference) ProtoMessage() {} func (*DiscoveredImageReference) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{13} + return fileDescriptor_e26b7f7bbc391025, []int{14} } func (m *DiscoveredImageReference) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -425,7 +453,7 @@ var xxx_messageInfo_DiscoveredImageReference proto.InternalMessageInfo func (m *Freight) Reset() { *m = Freight{} } func (*Freight) ProtoMessage() {} func (*Freight) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{14} + return fileDescriptor_e26b7f7bbc391025, []int{15} } func (m *Freight) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -453,7 +481,7 @@ var xxx_messageInfo_Freight proto.InternalMessageInfo func (m *FreightCollection) Reset() { *m = FreightCollection{} } func (*FreightCollection) ProtoMessage() {} func (*FreightCollection) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{15} + return fileDescriptor_e26b7f7bbc391025, []int{16} } func (m *FreightCollection) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -481,7 +509,7 @@ var xxx_messageInfo_FreightCollection proto.InternalMessageInfo func (m *FreightList) Reset() { *m = FreightList{} } func (*FreightList) ProtoMessage() {} func (*FreightList) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{16} + return fileDescriptor_e26b7f7bbc391025, []int{17} } func (m *FreightList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -509,7 +537,7 @@ var xxx_messageInfo_FreightList proto.InternalMessageInfo func (m *FreightOrigin) Reset() { *m = FreightOrigin{} } func (*FreightOrigin) ProtoMessage() {} func (*FreightOrigin) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{17} + return fileDescriptor_e26b7f7bbc391025, []int{18} } func (m *FreightOrigin) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -537,7 +565,7 @@ var xxx_messageInfo_FreightOrigin proto.InternalMessageInfo func (m *FreightReference) Reset() { *m = FreightReference{} } func (*FreightReference) ProtoMessage() {} func (*FreightReference) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{18} + return fileDescriptor_e26b7f7bbc391025, []int{19} } func (m *FreightReference) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -565,7 +593,7 @@ var xxx_messageInfo_FreightReference proto.InternalMessageInfo func (m *FreightRequest) Reset() { *m = FreightRequest{} } func (*FreightRequest) ProtoMessage() {} func (*FreightRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{19} + return fileDescriptor_e26b7f7bbc391025, []int{20} } func (m *FreightRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -593,7 +621,7 @@ var xxx_messageInfo_FreightRequest proto.InternalMessageInfo func (m *FreightSources) Reset() { *m = FreightSources{} } func (*FreightSources) ProtoMessage() {} func (*FreightSources) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{20} + return fileDescriptor_e26b7f7bbc391025, []int{21} } func (m *FreightSources) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -621,7 +649,7 @@ var xxx_messageInfo_FreightSources proto.InternalMessageInfo func (m *FreightStatus) Reset() { *m = FreightStatus{} } func (*FreightStatus) ProtoMessage() {} func (*FreightStatus) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{21} + return fileDescriptor_e26b7f7bbc391025, []int{22} } func (m *FreightStatus) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -649,7 +677,7 @@ var xxx_messageInfo_FreightStatus proto.InternalMessageInfo func (m *GitCommit) Reset() { *m = GitCommit{} } func (*GitCommit) ProtoMessage() {} func (*GitCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{22} + return fileDescriptor_e26b7f7bbc391025, []int{23} } func (m *GitCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -677,7 +705,7 @@ var xxx_messageInfo_GitCommit proto.InternalMessageInfo func (m *GitDiscoveryResult) Reset() { *m = GitDiscoveryResult{} } func (*GitDiscoveryResult) ProtoMessage() {} func (*GitDiscoveryResult) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{23} + return fileDescriptor_e26b7f7bbc391025, []int{24} } func (m *GitDiscoveryResult) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -705,7 +733,7 @@ var xxx_messageInfo_GitDiscoveryResult proto.InternalMessageInfo func (m *GitSubscription) Reset() { *m = GitSubscription{} } func (*GitSubscription) ProtoMessage() {} func (*GitSubscription) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{24} + return fileDescriptor_e26b7f7bbc391025, []int{25} } func (m *GitSubscription) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -733,7 +761,7 @@ var xxx_messageInfo_GitSubscription proto.InternalMessageInfo func (m *Health) Reset() { *m = Health{} } func (*Health) ProtoMessage() {} func (*Health) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{25} + return fileDescriptor_e26b7f7bbc391025, []int{26} } func (m *Health) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -761,7 +789,7 @@ var xxx_messageInfo_Health proto.InternalMessageInfo func (m *HealthCheckStep) Reset() { *m = HealthCheckStep{} } func (*HealthCheckStep) ProtoMessage() {} func (*HealthCheckStep) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{26} + return fileDescriptor_e26b7f7bbc391025, []int{27} } func (m *HealthCheckStep) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -789,7 +817,7 @@ var xxx_messageInfo_HealthCheckStep proto.InternalMessageInfo func (m *Image) Reset() { *m = Image{} } func (*Image) ProtoMessage() {} func (*Image) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{27} + return fileDescriptor_e26b7f7bbc391025, []int{28} } func (m *Image) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -817,7 +845,7 @@ var xxx_messageInfo_Image proto.InternalMessageInfo func (m *ImageDiscoveryResult) Reset() { *m = ImageDiscoveryResult{} } func (*ImageDiscoveryResult) ProtoMessage() {} func (*ImageDiscoveryResult) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{28} + return fileDescriptor_e26b7f7bbc391025, []int{29} } func (m *ImageDiscoveryResult) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -845,7 +873,7 @@ var xxx_messageInfo_ImageDiscoveryResult proto.InternalMessageInfo func (m *ImageSubscription) Reset() { *m = ImageSubscription{} } func (*ImageSubscription) ProtoMessage() {} func (*ImageSubscription) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{29} + return fileDescriptor_e26b7f7bbc391025, []int{30} } func (m *ImageSubscription) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -873,7 +901,7 @@ var xxx_messageInfo_ImageSubscription proto.InternalMessageInfo func (m *Project) Reset() { *m = Project{} } func (*Project) ProtoMessage() {} func (*Project) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{30} + return fileDescriptor_e26b7f7bbc391025, []int{31} } func (m *Project) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -901,7 +929,7 @@ var xxx_messageInfo_Project proto.InternalMessageInfo func (m *ProjectList) Reset() { *m = ProjectList{} } func (*ProjectList) ProtoMessage() {} func (*ProjectList) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{31} + return fileDescriptor_e26b7f7bbc391025, []int{32} } func (m *ProjectList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -929,7 +957,7 @@ var xxx_messageInfo_ProjectList proto.InternalMessageInfo func (m *ProjectSpec) Reset() { *m = ProjectSpec{} } func (*ProjectSpec) ProtoMessage() {} func (*ProjectSpec) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{32} + return fileDescriptor_e26b7f7bbc391025, []int{33} } func (m *ProjectSpec) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -957,7 +985,7 @@ var xxx_messageInfo_ProjectSpec proto.InternalMessageInfo func (m *ProjectStatus) Reset() { *m = ProjectStatus{} } func (*ProjectStatus) ProtoMessage() {} func (*ProjectStatus) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{33} + return fileDescriptor_e26b7f7bbc391025, []int{34} } func (m *ProjectStatus) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -985,7 +1013,7 @@ var xxx_messageInfo_ProjectStatus proto.InternalMessageInfo func (m *Promotion) Reset() { *m = Promotion{} } func (*Promotion) ProtoMessage() {} func (*Promotion) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{34} + return fileDescriptor_e26b7f7bbc391025, []int{35} } func (m *Promotion) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1013,7 +1041,7 @@ var xxx_messageInfo_Promotion proto.InternalMessageInfo func (m *PromotionList) Reset() { *m = PromotionList{} } func (*PromotionList) ProtoMessage() {} func (*PromotionList) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{35} + return fileDescriptor_e26b7f7bbc391025, []int{36} } func (m *PromotionList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1041,7 +1069,7 @@ var xxx_messageInfo_PromotionList proto.InternalMessageInfo func (m *PromotionPolicy) Reset() { *m = PromotionPolicy{} } func (*PromotionPolicy) ProtoMessage() {} func (*PromotionPolicy) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{36} + return fileDescriptor_e26b7f7bbc391025, []int{37} } func (m *PromotionPolicy) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1069,7 +1097,7 @@ var xxx_messageInfo_PromotionPolicy proto.InternalMessageInfo func (m *PromotionReference) Reset() { *m = PromotionReference{} } func (*PromotionReference) ProtoMessage() {} func (*PromotionReference) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{37} + return fileDescriptor_e26b7f7bbc391025, []int{38} } func (m *PromotionReference) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1097,7 +1125,7 @@ var xxx_messageInfo_PromotionReference proto.InternalMessageInfo func (m *PromotionSpec) Reset() { *m = PromotionSpec{} } func (*PromotionSpec) ProtoMessage() {} func (*PromotionSpec) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{38} + return fileDescriptor_e26b7f7bbc391025, []int{39} } func (m *PromotionSpec) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1125,7 +1153,7 @@ var xxx_messageInfo_PromotionSpec proto.InternalMessageInfo func (m *PromotionStatus) Reset() { *m = PromotionStatus{} } func (*PromotionStatus) ProtoMessage() {} func (*PromotionStatus) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{39} + return fileDescriptor_e26b7f7bbc391025, []int{40} } func (m *PromotionStatus) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1153,7 +1181,7 @@ var xxx_messageInfo_PromotionStatus proto.InternalMessageInfo func (m *PromotionStep) Reset() { *m = PromotionStep{} } func (*PromotionStep) ProtoMessage() {} func (*PromotionStep) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{40} + return fileDescriptor_e26b7f7bbc391025, []int{41} } func (m *PromotionStep) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1181,7 +1209,7 @@ var xxx_messageInfo_PromotionStep proto.InternalMessageInfo func (m *PromotionStepRetry) Reset() { *m = PromotionStepRetry{} } func (*PromotionStepRetry) ProtoMessage() {} func (*PromotionStepRetry) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{41} + return fileDescriptor_e26b7f7bbc391025, []int{42} } func (m *PromotionStepRetry) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1209,7 +1237,7 @@ var xxx_messageInfo_PromotionStepRetry proto.InternalMessageInfo func (m *PromotionTemplate) Reset() { *m = PromotionTemplate{} } func (*PromotionTemplate) ProtoMessage() {} func (*PromotionTemplate) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{42} + return fileDescriptor_e26b7f7bbc391025, []int{43} } func (m *PromotionTemplate) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1237,7 +1265,7 @@ var xxx_messageInfo_PromotionTemplate proto.InternalMessageInfo func (m *PromotionTemplateSpec) Reset() { *m = PromotionTemplateSpec{} } func (*PromotionTemplateSpec) ProtoMessage() {} func (*PromotionTemplateSpec) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{43} + return fileDescriptor_e26b7f7bbc391025, []int{44} } func (m *PromotionTemplateSpec) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1265,7 +1293,7 @@ var xxx_messageInfo_PromotionTemplateSpec proto.InternalMessageInfo func (m *PromotionVariable) Reset() { *m = PromotionVariable{} } func (*PromotionVariable) ProtoMessage() {} func (*PromotionVariable) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{44} + return fileDescriptor_e26b7f7bbc391025, []int{45} } func (m *PromotionVariable) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1293,7 +1321,7 @@ var xxx_messageInfo_PromotionVariable proto.InternalMessageInfo func (m *RepoSubscription) Reset() { *m = RepoSubscription{} } func (*RepoSubscription) ProtoMessage() {} func (*RepoSubscription) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{45} + return fileDescriptor_e26b7f7bbc391025, []int{46} } func (m *RepoSubscription) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1321,7 +1349,7 @@ var xxx_messageInfo_RepoSubscription proto.InternalMessageInfo func (m *Stage) Reset() { *m = Stage{} } func (*Stage) ProtoMessage() {} func (*Stage) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{46} + return fileDescriptor_e26b7f7bbc391025, []int{47} } func (m *Stage) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1349,7 +1377,7 @@ var xxx_messageInfo_Stage proto.InternalMessageInfo func (m *StageList) Reset() { *m = StageList{} } func (*StageList) ProtoMessage() {} func (*StageList) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{47} + return fileDescriptor_e26b7f7bbc391025, []int{48} } func (m *StageList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1377,7 +1405,7 @@ var xxx_messageInfo_StageList proto.InternalMessageInfo func (m *StageSpec) Reset() { *m = StageSpec{} } func (*StageSpec) ProtoMessage() {} func (*StageSpec) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{48} + return fileDescriptor_e26b7f7bbc391025, []int{49} } func (m *StageSpec) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1405,7 +1433,7 @@ var xxx_messageInfo_StageSpec proto.InternalMessageInfo func (m *StageStatus) Reset() { *m = StageStatus{} } func (*StageStatus) ProtoMessage() {} func (*StageStatus) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{49} + return fileDescriptor_e26b7f7bbc391025, []int{50} } func (m *StageStatus) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1433,7 +1461,7 @@ var xxx_messageInfo_StageStatus proto.InternalMessageInfo func (m *StepExecutionMetadata) Reset() { *m = StepExecutionMetadata{} } func (*StepExecutionMetadata) ProtoMessage() {} func (*StepExecutionMetadata) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{50} + return fileDescriptor_e26b7f7bbc391025, []int{51} } func (m *StepExecutionMetadata) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1461,7 +1489,7 @@ var xxx_messageInfo_StepExecutionMetadata proto.InternalMessageInfo func (m *Verification) Reset() { *m = Verification{} } func (*Verification) ProtoMessage() {} func (*Verification) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{51} + return fileDescriptor_e26b7f7bbc391025, []int{52} } func (m *Verification) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1489,7 +1517,7 @@ var xxx_messageInfo_Verification proto.InternalMessageInfo func (m *VerificationInfo) Reset() { *m = VerificationInfo{} } func (*VerificationInfo) ProtoMessage() {} func (*VerificationInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{52} + return fileDescriptor_e26b7f7bbc391025, []int{53} } func (m *VerificationInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1517,7 +1545,7 @@ var xxx_messageInfo_VerificationInfo proto.InternalMessageInfo func (m *VerifiedStage) Reset() { *m = VerifiedStage{} } func (*VerifiedStage) ProtoMessage() {} func (*VerifiedStage) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{53} + return fileDescriptor_e26b7f7bbc391025, []int{54} } func (m *VerifiedStage) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1545,7 +1573,7 @@ var xxx_messageInfo_VerifiedStage proto.InternalMessageInfo func (m *Warehouse) Reset() { *m = Warehouse{} } func (*Warehouse) ProtoMessage() {} func (*Warehouse) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{54} + return fileDescriptor_e26b7f7bbc391025, []int{55} } func (m *Warehouse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1573,7 +1601,7 @@ var xxx_messageInfo_Warehouse proto.InternalMessageInfo func (m *WarehouseList) Reset() { *m = WarehouseList{} } func (*WarehouseList) ProtoMessage() {} func (*WarehouseList) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{55} + return fileDescriptor_e26b7f7bbc391025, []int{56} } func (m *WarehouseList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1601,7 +1629,7 @@ var xxx_messageInfo_WarehouseList proto.InternalMessageInfo func (m *WarehouseSpec) Reset() { *m = WarehouseSpec{} } func (*WarehouseSpec) ProtoMessage() {} func (*WarehouseSpec) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{56} + return fileDescriptor_e26b7f7bbc391025, []int{57} } func (m *WarehouseSpec) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1629,7 +1657,7 @@ var xxx_messageInfo_WarehouseSpec proto.InternalMessageInfo func (m *WarehouseStatus) Reset() { *m = WarehouseStatus{} } func (*WarehouseStatus) ProtoMessage() {} func (*WarehouseStatus) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{57} + return fileDescriptor_e26b7f7bbc391025, []int{58} } func (m *WarehouseStatus) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1668,6 +1696,7 @@ func init() { proto.RegisterType((*Chart)(nil), "github.com.akuity.kargo.api.v1alpha1.Chart") proto.RegisterType((*ChartDiscoveryResult)(nil), "github.com.akuity.kargo.api.v1alpha1.ChartDiscoveryResult") proto.RegisterType((*ChartSubscription)(nil), "github.com.akuity.kargo.api.v1alpha1.ChartSubscription") + proto.RegisterType((*CurrentStage)(nil), "github.com.akuity.kargo.api.v1alpha1.CurrentStage") proto.RegisterType((*DiscoveredArtifacts)(nil), "github.com.akuity.kargo.api.v1alpha1.DiscoveredArtifacts") proto.RegisterType((*DiscoveredCommit)(nil), "github.com.akuity.kargo.api.v1alpha1.DiscoveredCommit") proto.RegisterType((*DiscoveredImageReference)(nil), "github.com.akuity.kargo.api.v1alpha1.DiscoveredImageReference") @@ -1681,6 +1710,7 @@ func init() { proto.RegisterType((*FreightSources)(nil), "github.com.akuity.kargo.api.v1alpha1.FreightSources") proto.RegisterType((*FreightStatus)(nil), "github.com.akuity.kargo.api.v1alpha1.FreightStatus") proto.RegisterMapType((map[string]ApprovedStage)(nil), "github.com.akuity.kargo.api.v1alpha1.FreightStatus.ApprovedForEntry") + proto.RegisterMapType((map[string]CurrentStage)(nil), "github.com.akuity.kargo.api.v1alpha1.FreightStatus.CurrentlyInEntry") proto.RegisterMapType((map[string]VerifiedStage)(nil), "github.com.akuity.kargo.api.v1alpha1.FreightStatus.VerifiedInEntry") proto.RegisterType((*GitCommit)(nil), "github.com.akuity.kargo.api.v1alpha1.GitCommit") proto.RegisterType((*GitDiscoveryResult)(nil), "github.com.akuity.kargo.api.v1alpha1.GitDiscoveryResult") @@ -1725,245 +1755,253 @@ func init() { } var fileDescriptor_e26b7f7bbc391025 = []byte{ - // 3803 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x5c, 0xcd, 0x6f, 0x1c, 0x47, - 0x76, 0x57, 0xcf, 0x90, 0x43, 0xce, 0x1b, 0x52, 0x24, 0x4b, 0x94, 0xcd, 0xe5, 0xc6, 0xa4, 0xd2, - 0x6b, 0x18, 0x76, 0x6c, 0x0f, 0x23, 0xd9, 0xb2, 0x64, 0x39, 0x51, 0xc0, 0x21, 0x25, 0x99, 0x0a, - 0xd7, 0x62, 0x6a, 0x64, 0x79, 0x57, 0x6b, 0xc3, 0x29, 0xce, 0x14, 0x67, 0x3a, 0x9c, 0xe9, 0x9e, - 0xad, 0xaa, 0xe1, 0x9a, 0x49, 0x90, 0x6c, 0xbe, 0x80, 0x45, 0xbe, 0xb0, 0x07, 0x03, 0xde, 0x00, - 0x09, 0x10, 0x24, 0xc7, 0x45, 0xf2, 0x0f, 0xe4, 0xe0, 0x43, 0x2e, 0x46, 0xb0, 0x08, 0x8c, 0x24, - 0x07, 0x1f, 0x16, 0x44, 0xcc, 0x05, 0x72, 0xdc, 0x5b, 0x2e, 0x02, 0x02, 0x04, 0xf5, 0xd1, 0xdd, - 0xd5, 0x3d, 0x3d, 0xe2, 0xf4, 0x88, 0x14, 0x8c, 0xbd, 0x0d, 0xeb, 0xd5, 0xfb, 0xbd, 0xfa, 0x78, - 0xef, 0xd5, 0x7b, 0xaf, 0xaa, 0x09, 0xaf, 0xb7, 0x3c, 0xd1, 0xee, 0xef, 0x56, 0x1b, 0x41, 0x77, - 0x8d, 0xec, 0xf7, 0x3d, 0x71, 0xb8, 0xb6, 0x4f, 0x58, 0x2b, 0x58, 0x23, 0x3d, 0x6f, 0xed, 0xe0, - 0x32, 0xe9, 0xf4, 0xda, 0xe4, 0xf2, 0x5a, 0x8b, 0xfa, 0x94, 0x11, 0x41, 0x9b, 0xd5, 0x1e, 0x0b, - 0x44, 0x80, 0x9e, 0x8f, 0xb9, 0xaa, 0x9a, 0xab, 0xaa, 0xb8, 0xaa, 0xa4, 0xe7, 0x55, 0x43, 0xae, - 0xe5, 0x57, 0x2d, 0xec, 0x56, 0xd0, 0x0a, 0xd6, 0x14, 0xf3, 0x6e, 0x7f, 0x4f, 0xfd, 0xa5, 0xfe, - 0x50, 0xbf, 0x34, 0xe8, 0xf2, 0xdb, 0xfb, 0xd7, 0x79, 0xd5, 0x53, 0x92, 0xe9, 0x47, 0x82, 0xfa, - 0xdc, 0x0b, 0x7c, 0xfe, 0x2a, 0xe9, 0x79, 0x9c, 0xb2, 0x03, 0xca, 0xd6, 0x7a, 0xfb, 0x2d, 0x49, - 0xe3, 0xc9, 0x0e, 0x6b, 0x07, 0x03, 0xc3, 0x5b, 0x7e, 0x3d, 0x46, 0xea, 0x92, 0x46, 0xdb, 0xf3, - 0x29, 0x3b, 0x8c, 0xd9, 0xbb, 0x54, 0x90, 0x2c, 0xae, 0xb5, 0x61, 0x5c, 0xac, 0xef, 0x0b, 0xaf, - 0x4b, 0x07, 0x18, 0xde, 0x38, 0x89, 0x81, 0x37, 0xda, 0xb4, 0x4b, 0xd2, 0x7c, 0xee, 0xfb, 0x70, - 0x61, 0xdd, 0x27, 0x9d, 0x43, 0xee, 0x71, 0xdc, 0xf7, 0xd7, 0x59, 0xab, 0xdf, 0xa5, 0xbe, 0x40, - 0x97, 0x60, 0xc2, 0x27, 0x5d, 0xba, 0xe4, 0x5c, 0x72, 0x5e, 0x2c, 0xd7, 0x66, 0x3e, 0x3b, 0x5a, - 0x3d, 0x77, 0x7c, 0xb4, 0x3a, 0xf1, 0x0e, 0xe9, 0x52, 0xac, 0x28, 0xe8, 0x1b, 0x30, 0x79, 0x40, - 0x3a, 0x7d, 0xba, 0x54, 0x50, 0x5d, 0x66, 0x4d, 0x97, 0xc9, 0x07, 0xb2, 0x11, 0x6b, 0x9a, 0xfb, - 0x27, 0xc5, 0x04, 0xfc, 0x37, 0xa9, 0x20, 0x4d, 0x22, 0x08, 0xea, 0x42, 0xa9, 0x43, 0x76, 0x69, - 0x87, 0x2f, 0x39, 0x97, 0x8a, 0x2f, 0x56, 0xae, 0xdc, 0xaa, 0x8e, 0xb2, 0x89, 0xd5, 0x0c, 0xa8, - 0xea, 0xb6, 0xc2, 0xb9, 0xe5, 0x0b, 0x76, 0x58, 0x3b, 0x6f, 0x06, 0x51, 0xd2, 0x8d, 0xd8, 0x08, - 0x41, 0x7f, 0xe4, 0x40, 0x85, 0xf8, 0x7e, 0x20, 0x88, 0x90, 0xdb, 0xb4, 0x54, 0x50, 0x42, 0xef, - 0x8e, 0x2f, 0x74, 0x3d, 0x06, 0xd3, 0x92, 0x2f, 0x18, 0xc9, 0x15, 0x8b, 0x82, 0x6d, 0x99, 0xcb, - 0x6f, 0x42, 0xc5, 0x1a, 0x2a, 0x9a, 0x87, 0xe2, 0x3e, 0x3d, 0xd4, 0xeb, 0x8b, 0xe5, 0x4f, 0xb4, - 0x98, 0x58, 0x50, 0xb3, 0x82, 0x37, 0x0a, 0xd7, 0x9d, 0xe5, 0x9b, 0x30, 0x9f, 0x16, 0x98, 0x87, - 0xdf, 0xfd, 0x6b, 0x07, 0x16, 0xad, 0x59, 0x60, 0xba, 0x47, 0x19, 0xf5, 0x1b, 0x14, 0xad, 0x41, - 0x59, 0xee, 0x25, 0xef, 0x91, 0x46, 0xb8, 0xd5, 0x0b, 0x66, 0x22, 0xe5, 0x77, 0x42, 0x02, 0x8e, - 0xfb, 0x44, 0x6a, 0x51, 0x78, 0x9c, 0x5a, 0xf4, 0xda, 0x84, 0xd3, 0xa5, 0x62, 0x52, 0x2d, 0x76, - 0x64, 0x23, 0xd6, 0x34, 0xf7, 0xd7, 0xe1, 0x6b, 0xe1, 0x78, 0xee, 0xd3, 0x6e, 0xaf, 0x43, 0x04, - 0x8d, 0x07, 0x75, 0xa2, 0xea, 0xb9, 0xfb, 0x30, 0xbb, 0xde, 0xeb, 0xb1, 0xe0, 0x80, 0x36, 0xeb, - 0x82, 0xb4, 0x28, 0x7a, 0x08, 0x40, 0x4c, 0xc3, 0xba, 0x50, 0x8c, 0x95, 0x2b, 0xbf, 0x52, 0xd5, - 0x16, 0x51, 0xb5, 0x2d, 0xa2, 0xda, 0xdb, 0x6f, 0xc9, 0x06, 0x5e, 0x95, 0x86, 0x57, 0x3d, 0xb8, - 0x5c, 0xbd, 0xef, 0x75, 0x69, 0xed, 0xfc, 0xf1, 0xd1, 0x2a, 0xac, 0x47, 0x08, 0xd8, 0x42, 0x73, - 0xff, 0xd8, 0x81, 0x8b, 0xeb, 0xac, 0x15, 0x6c, 0x6c, 0xae, 0xf7, 0x7a, 0x6f, 0x53, 0xd2, 0x11, - 0xed, 0xba, 0x20, 0xa2, 0xcf, 0xd1, 0x4d, 0x28, 0x71, 0xf5, 0xcb, 0x0c, 0xf5, 0x85, 0x50, 0xfb, - 0x34, 0xfd, 0xd1, 0xd1, 0xea, 0x62, 0x06, 0x23, 0xc5, 0x86, 0x0b, 0xbd, 0x04, 0x53, 0x5d, 0xca, - 0x39, 0x69, 0x85, 0xeb, 0x39, 0x67, 0x00, 0xa6, 0xbe, 0xa9, 0x9b, 0x71, 0x48, 0x77, 0xff, 0xad, - 0x00, 0x73, 0x11, 0x96, 0x11, 0x7f, 0x06, 0x9b, 0xd7, 0x87, 0x99, 0xb6, 0x35, 0x43, 0xb5, 0x87, - 0x95, 0x2b, 0x6f, 0x8d, 0x68, 0x27, 0x59, 0x8b, 0x54, 0x5b, 0x34, 0x62, 0x66, 0xec, 0x56, 0x9c, - 0x10, 0x83, 0xba, 0x00, 0xfc, 0xd0, 0x6f, 0x18, 0xa1, 0x13, 0x4a, 0xe8, 0x9b, 0x39, 0x85, 0xd6, - 0x23, 0x80, 0x1a, 0x32, 0x22, 0x21, 0x6e, 0xc3, 0x96, 0x00, 0xf7, 0x9f, 0x1d, 0xb8, 0x90, 0xc1, - 0x87, 0x7e, 0x2d, 0xb5, 0x9f, 0xcf, 0x0f, 0xec, 0x27, 0x1a, 0x60, 0x8b, 0x77, 0xf3, 0x15, 0x98, - 0x66, 0xf4, 0xc0, 0x93, 0xe7, 0x80, 0x59, 0xe1, 0x79, 0xc3, 0x3f, 0x8d, 0x4d, 0x3b, 0x8e, 0x7a, - 0xa0, 0x97, 0xa1, 0x1c, 0xfe, 0x96, 0xcb, 0x5c, 0x94, 0xa6, 0x22, 0x37, 0x2e, 0xec, 0xca, 0x71, - 0x4c, 0x77, 0xff, 0x10, 0x26, 0x37, 0xda, 0x84, 0x09, 0xa9, 0x31, 0x8c, 0xf6, 0x82, 0x77, 0xf1, - 0xb6, 0x19, 0x62, 0xa4, 0x31, 0x58, 0x37, 0xe3, 0x90, 0x3e, 0xc2, 0x66, 0xbf, 0x04, 0x53, 0x07, - 0x94, 0xa9, 0xf1, 0x16, 0x93, 0x60, 0x0f, 0x74, 0x33, 0x0e, 0xe9, 0xee, 0x7f, 0x3a, 0xb0, 0xa8, - 0x46, 0xb0, 0xe9, 0xf1, 0x46, 0x70, 0x40, 0xd9, 0x21, 0xa6, 0xbc, 0xdf, 0x39, 0xe5, 0x01, 0x6d, - 0xc2, 0x3c, 0xa7, 0xdd, 0x03, 0xca, 0x36, 0x02, 0x9f, 0x0b, 0x46, 0x3c, 0x5f, 0x98, 0x91, 0x2d, - 0x99, 0xde, 0xf3, 0xf5, 0x14, 0x1d, 0x0f, 0x70, 0xa0, 0x17, 0x61, 0xda, 0x0c, 0x5b, 0xaa, 0x92, - 0x5c, 0xd8, 0x19, 0xb9, 0x07, 0x66, 0x4e, 0x1c, 0x47, 0x54, 0xf7, 0x7f, 0x1c, 0x58, 0x50, 0xb3, - 0xaa, 0xf7, 0x77, 0x79, 0x83, 0x79, 0x3d, 0xe9, 0x5e, 0xbf, 0x8a, 0x53, 0xba, 0x09, 0xe7, 0x9b, - 0xe1, 0xc2, 0x6f, 0x7b, 0x5d, 0x4f, 0x28, 0x1b, 0x99, 0xac, 0x3d, 0x63, 0x30, 0xce, 0x6f, 0x26, - 0xa8, 0x38, 0xd5, 0xdb, 0xfd, 0xdb, 0x22, 0x5c, 0x08, 0xbb, 0xd0, 0xe6, 0x3a, 0x13, 0xde, 0x1e, - 0x69, 0x08, 0x8e, 0x9a, 0x30, 0xd3, 0x8c, 0x9b, 0x85, 0xb1, 0xbc, 0x3c, 0x8e, 0x33, 0xb2, 0x6e, - 0x0b, 0x5e, 0xe0, 0x04, 0x2a, 0x7a, 0x0f, 0x8a, 0x2d, 0x4f, 0x98, 0x83, 0xfe, 0xfa, 0x68, 0x66, - 0x7d, 0xc7, 0x4b, 0xab, 0x5a, 0xad, 0x62, 0x44, 0x15, 0xef, 0x78, 0x02, 0x4b, 0x44, 0xb4, 0x0b, - 0x25, 0xaf, 0x4b, 0x5a, 0x34, 0x3c, 0xcf, 0x6f, 0x8c, 0x86, 0xbd, 0x25, 0x79, 0xd2, 0xe8, 0x51, - 0xe4, 0xa0, 0xa8, 0x1c, 0x1b, 0x64, 0x29, 0xa3, 0x21, 0x55, 0x44, 0x1b, 0xe9, 0xc8, 0x32, 0xb2, - 0x8c, 0x25, 0x96, 0xa1, 0xa8, 0x1c, 0x1b, 0x64, 0xf7, 0x8b, 0x02, 0xcc, 0xc7, 0xeb, 0xb7, 0x11, - 0x74, 0xbb, 0x9e, 0x40, 0xcb, 0x50, 0xf0, 0x9a, 0x46, 0x03, 0xc1, 0x30, 0x16, 0xb6, 0x36, 0x71, - 0xc1, 0x6b, 0xa2, 0x17, 0xa0, 0xb4, 0xcb, 0x88, 0xdf, 0x68, 0x1b, 0xcd, 0x8b, 0x80, 0x6b, 0xaa, - 0x15, 0x1b, 0x2a, 0x7a, 0x0e, 0x8a, 0x82, 0xb4, 0x8c, 0xc2, 0x45, 0xeb, 0x77, 0x9f, 0xb4, 0xb0, - 0x6c, 0x97, 0x9a, 0xce, 0xfb, 0xbb, 0xbf, 0x43, 0x1b, 0x7a, 0xe7, 0x2d, 0x4d, 0xaf, 0xeb, 0x66, - 0x1c, 0xd2, 0xa5, 0x44, 0xd2, 0x17, 0xed, 0x80, 0x2d, 0x4d, 0x26, 0x25, 0xae, 0xab, 0x56, 0x6c, - 0xa8, 0xf2, 0x4c, 0x6a, 0xa8, 0xf1, 0x0b, 0xca, 0x96, 0x4a, 0xc9, 0x33, 0x69, 0x23, 0x24, 0xe0, - 0xb8, 0x0f, 0xfa, 0x00, 0x2a, 0x0d, 0x46, 0x89, 0x08, 0xd8, 0x26, 0x11, 0x74, 0x69, 0x2a, 0xb7, - 0x06, 0xce, 0xc9, 0xa0, 0x6b, 0x23, 0x86, 0xc0, 0x36, 0x9e, 0xfb, 0x73, 0x07, 0x96, 0xe2, 0xa5, - 0x55, 0x7b, 0x1b, 0x07, 0x1a, 0x66, 0x79, 0x9c, 0x21, 0xcb, 0xf3, 0x02, 0x94, 0x9a, 0x5e, 0x8b, - 0x72, 0x91, 0x5e, 0xe5, 0x4d, 0xd5, 0x8a, 0x0d, 0x15, 0x5d, 0x01, 0x68, 0x79, 0xc2, 0x38, 0x07, - 0xb3, 0xd8, 0xd1, 0x11, 0x74, 0x27, 0xa2, 0x60, 0xab, 0x17, 0x7a, 0x0f, 0xca, 0x6a, 0x98, 0x63, - 0x9a, 0x9d, 0x3a, 0x2a, 0x36, 0x42, 0x00, 0x1c, 0x63, 0xb9, 0x9f, 0x4f, 0xc0, 0xd4, 0x6d, 0x46, - 0xbd, 0x56, 0x5b, 0xa0, 0xdf, 0x86, 0xe9, 0xae, 0x09, 0x58, 0x4d, 0x4c, 0xf4, 0xab, 0xa3, 0xc9, - 0xb8, 0xa7, 0x36, 0x5d, 0x06, 0xbb, 0xf1, 0x44, 0xe2, 0x36, 0x1c, 0xa1, 0xca, 0x60, 0x8f, 0x74, - 0x3c, 0xc2, 0xd5, 0xbe, 0x59, 0xc1, 0xde, 0xba, 0x6c, 0xc4, 0x9a, 0x86, 0xbe, 0x03, 0xa5, 0x80, - 0x79, 0x2d, 0xcf, 0x5f, 0x2a, 0xab, 0x41, 0xbc, 0x36, 0x9a, 0x09, 0x99, 0x59, 0xdc, 0x53, 0xac, - 0xf1, 0xe2, 0xeb, 0xbf, 0xb1, 0x81, 0x44, 0x0f, 0x61, 0x4a, 0x2b, 0x53, 0x68, 0xa0, 0x6b, 0x23, - 0x3b, 0x18, 0xad, 0x8f, 0xb1, 0xd2, 0xeb, 0xbf, 0x39, 0x0e, 0x01, 0x51, 0x3d, 0xf2, 0x2f, 0x13, - 0x0a, 0xfa, 0xe5, 0x1c, 0xfe, 0x65, 0xa8, 0x43, 0xa9, 0x47, 0x0e, 0x65, 0x32, 0x0f, 0xa8, 0x72, - 0x19, 0xc3, 0x3c, 0x88, 0x5c, 0x62, 0x13, 0xb9, 0x94, 0xc6, 0x58, 0x62, 0x13, 0x36, 0x9d, 0x4f, - 0x86, 0x3b, 0x61, 0x60, 0xe3, 0x7e, 0x5c, 0x84, 0x05, 0xd3, 0x73, 0x23, 0xe8, 0x74, 0x68, 0x43, - 0x1d, 0x93, 0xda, 0x3f, 0x15, 0x33, 0xfd, 0x93, 0x07, 0x93, 0x9e, 0xa0, 0xdd, 0x30, 0xb9, 0xab, - 0xe5, 0x1a, 0x4d, 0x2c, 0xa3, 0xba, 0x25, 0x41, 0x74, 0x7e, 0x15, 0xed, 0x92, 0xe9, 0x85, 0xb5, - 0x04, 0xf4, 0x67, 0x0e, 0x5c, 0x38, 0xa0, 0xcc, 0xdb, 0xf3, 0x1a, 0x2a, 0x3b, 0x7a, 0xdb, 0xe3, - 0x22, 0x60, 0x87, 0xe6, 0x44, 0x78, 0x63, 0x34, 0xc9, 0x0f, 0x2c, 0x80, 0x2d, 0x7f, 0x2f, 0xa8, - 0x7d, 0xdd, 0x48, 0xbb, 0xf0, 0x60, 0x10, 0x1a, 0x67, 0xc9, 0x5b, 0xee, 0x01, 0xc4, 0xa3, 0xcd, - 0x48, 0xce, 0xb6, 0xed, 0xe4, 0x6c, 0xe4, 0x81, 0x85, 0x93, 0x0d, 0x5d, 0x96, 0x9d, 0xd4, 0x7d, - 0xea, 0x40, 0xc5, 0xd0, 0xb7, 0x3d, 0x2e, 0xd0, 0xfb, 0x03, 0xd6, 0x5e, 0x1d, 0xcd, 0xda, 0x25, - 0xb7, 0xb2, 0xf5, 0x28, 0x5e, 0x0d, 0x5b, 0x2c, 0x4b, 0xc7, 0xe1, 0x96, 0xea, 0x85, 0x7d, 0x35, - 0xd7, 0xf8, 0x63, 0xc7, 0xa0, 0xd6, 0xc8, 0xec, 0x9d, 0xcb, 0x60, 0x36, 0x61, 0xe4, 0xe8, 0x2a, - 0x4c, 0xec, 0x7b, 0x7e, 0x78, 0xea, 0xfd, 0x72, 0x18, 0x4f, 0xfd, 0xa6, 0xe7, 0x37, 0x1f, 0x1d, - 0xad, 0x2e, 0x24, 0x3a, 0xcb, 0x46, 0xac, 0xba, 0x9f, 0x1c, 0x86, 0xdd, 0x98, 0xfe, 0xd1, 0xdf, - 0xaf, 0x9e, 0xfb, 0xfe, 0x4f, 0x2f, 0x9d, 0x73, 0x3f, 0x29, 0xc2, 0x7c, 0x7a, 0x55, 0x47, 0x28, - 0x76, 0xc4, 0x3e, 0x6c, 0xfa, 0x4c, 0x7d, 0x58, 0xe1, 0xec, 0x7c, 0x58, 0xf1, 0x2c, 0x7c, 0xd8, - 0xc4, 0xa9, 0xf9, 0x30, 0xf7, 0xdf, 0x1d, 0x38, 0x1f, 0xed, 0xcc, 0x77, 0xfb, 0xf2, 0x64, 0x8d, - 0x57, 0xdd, 0x39, 0xfd, 0x55, 0xff, 0x10, 0xa6, 0x78, 0xd0, 0x67, 0x0d, 0x15, 0x3e, 0x4a, 0xf4, - 0xd7, 0xf3, 0x39, 0x4d, 0xcd, 0x6b, 0xc5, 0x4c, 0xba, 0x01, 0x87, 0xa8, 0xee, 0xfb, 0xd1, 0x7c, - 0x0c, 0x49, 0x47, 0x14, 0x4c, 0xc6, 0x5b, 0x72, 0x3e, 0xd3, 0x76, 0x44, 0x21, 0x5b, 0xb1, 0xa1, - 0x22, 0x57, 0xb9, 0xf3, 0x30, 0xb0, 0x2d, 0xd7, 0xc0, 0x78, 0x65, 0xb5, 0x07, 0x9a, 0xe2, 0xfe, - 0xbc, 0x18, 0x59, 0x8f, 0x49, 0x5f, 0xbf, 0x07, 0xa0, 0x3d, 0x13, 0x6d, 0x6e, 0xf9, 0xc6, 0xf5, - 0x6e, 0x8c, 0x71, 0x10, 0x18, 0x77, 0x28, 0x51, 0xb4, 0xef, 0x8d, 0x62, 0x80, 0x98, 0x80, 0x2d, - 0x51, 0xe8, 0xf7, 0xa0, 0x12, 0xd6, 0x4b, 0x6e, 0x07, 0xcc, 0xe8, 0xf0, 0xe6, 0x38, 0x92, 0xd7, - 0x63, 0x98, 0x74, 0x59, 0x2d, 0xa6, 0x60, 0x5b, 0xda, 0x32, 0x83, 0xb9, 0xd4, 0x78, 0x33, 0xbc, - 0xef, 0x56, 0xd2, 0xfb, 0xbe, 0x96, 0xe7, 0x58, 0x30, 0x35, 0x26, 0xbb, 0x1e, 0xc7, 0x61, 0x3e, - 0x3d, 0xd2, 0x53, 0x13, 0x9a, 0x28, 0x6c, 0xd9, 0xfe, 0xfe, 0xef, 0x0a, 0x50, 0x8e, 0x2c, 0x3e, - 0x4f, 0x96, 0xaa, 0x4f, 0xea, 0xc2, 0x09, 0x99, 0x44, 0x71, 0x94, 0x4c, 0x62, 0x62, 0x78, 0x26, - 0x11, 0x56, 0xb2, 0x4a, 0x8f, 0xaf, 0x64, 0x59, 0x99, 0xc4, 0xd4, 0xe8, 0x99, 0xc4, 0xf4, 0xc9, - 0x99, 0x84, 0xfb, 0x0f, 0x0e, 0xa0, 0xc1, 0xb4, 0x31, 0xcf, 0x42, 0x91, 0xb4, 0x1f, 0x1e, 0xf1, - 0x94, 0x4e, 0xe7, 0x6e, 0xc3, 0xdd, 0xb1, 0xfb, 0xe9, 0x24, 0xcc, 0xdd, 0xf1, 0xc6, 0x2e, 0x38, - 0x08, 0x78, 0x56, 0x23, 0xd5, 0xa9, 0x89, 0x91, 0xea, 0x82, 0x11, 0x41, 0x5b, 0x87, 0x66, 0x7f, - 0x6f, 0x18, 0xd6, 0x67, 0x37, 0xb2, 0xbb, 0x3d, 0x1a, 0x4e, 0xc2, 0xc3, 0xa0, 0x47, 0x56, 0x92, - 0xb7, 0x60, 0x96, 0x0b, 0xe6, 0x35, 0x84, 0x2e, 0x69, 0xf0, 0xa5, 0x8a, 0xf2, 0x72, 0x17, 0x4d, - 0xf7, 0xd9, 0xba, 0x4d, 0xc4, 0xc9, 0xbe, 0x99, 0x95, 0x92, 0x89, 0xdc, 0x95, 0x92, 0x35, 0x28, - 0x93, 0x4e, 0x27, 0xf8, 0xde, 0x7d, 0xd2, 0xe2, 0x26, 0x55, 0x8d, 0xb4, 0x66, 0x3d, 0x24, 0xe0, - 0xb8, 0x0f, 0xaa, 0x02, 0x78, 0x2d, 0x3f, 0x60, 0x54, 0x71, 0x94, 0x94, 0xbb, 0x55, 0xd5, 0xe0, - 0xad, 0xa8, 0x15, 0x5b, 0x3d, 0x50, 0x1d, 0x2e, 0x7a, 0x3e, 0xa7, 0x8d, 0x3e, 0xa3, 0xf5, 0x7d, - 0xaf, 0x77, 0x7f, 0xbb, 0xae, 0xbc, 0xc4, 0xa1, 0xd2, 0xe6, 0xe9, 0xda, 0x73, 0x46, 0xd8, 0xc5, - 0xad, 0xac, 0x4e, 0x38, 0x9b, 0x17, 0xbd, 0x0e, 0x33, 0x9e, 0xdf, 0xe8, 0xf4, 0x9b, 0x74, 0x87, - 0x88, 0x36, 0x5f, 0x9a, 0x56, 0xc3, 0x98, 0x3f, 0x3e, 0x5a, 0x9d, 0xd9, 0xb2, 0xda, 0x71, 0xa2, - 0x97, 0xe4, 0xa2, 0x1f, 0x59, 0x5c, 0xe5, 0x98, 0xeb, 0xd6, 0x47, 0x36, 0x97, 0xdd, 0x2b, 0xa3, - 0x96, 0x04, 0xb9, 0x6a, 0x49, 0x3f, 0x2e, 0x40, 0x49, 0x97, 0x72, 0xd1, 0xd5, 0x54, 0xbd, 0xf4, - 0xb9, 0x81, 0x7a, 0x69, 0x25, 0xab, 0xec, 0xed, 0x42, 0xc9, 0xe3, 0xbc, 0x9f, 0x3c, 0xdd, 0xb6, - 0x54, 0x0b, 0x36, 0x14, 0x55, 0x76, 0x09, 0xfc, 0x3d, 0xaf, 0x65, 0x92, 0xe3, 0x9b, 0x56, 0x28, - 0x1b, 0x5f, 0xb7, 0x7d, 0x18, 0xdd, 0xc7, 0xc5, 0x51, 0x6d, 0xa2, 0x83, 0x0c, 0x6f, 0xef, 0xd6, - 0xef, 0xbd, 0xa3, 0x65, 0x6c, 0x28, 0x44, 0x6c, 0x90, 0xa5, 0x8c, 0xa0, 0x2f, 0x7a, 0x7d, 0xa1, - 0x14, 0xe5, 0x94, 0x64, 0xdc, 0x53, 0x88, 0xd8, 0x20, 0xbb, 0x9f, 0x38, 0x30, 0xa7, 0xd7, 0x60, - 0xa3, 0x4d, 0x1b, 0xfb, 0x75, 0x41, 0x7b, 0x32, 0xda, 0xec, 0x73, 0xca, 0xd3, 0xd1, 0xe6, 0xbb, - 0x9c, 0x72, 0xac, 0x28, 0xd6, 0xec, 0x0b, 0x67, 0x35, 0x7b, 0xf7, 0x9f, 0x1c, 0x98, 0x54, 0x61, - 0x5d, 0x1e, 0xff, 0x93, 0x2c, 0x75, 0x14, 0x46, 0x2a, 0x75, 0x9c, 0x50, 0x84, 0x8a, 0xab, 0x2c, - 0x13, 0x8f, 0xab, 0xb2, 0xb8, 0x3f, 0x73, 0x60, 0x31, 0xab, 0x72, 0x97, 0x67, 0xf8, 0xaf, 0xc0, - 0x74, 0xaf, 0x43, 0xc4, 0x5e, 0xc0, 0xba, 0xe9, 0x12, 0xfd, 0x8e, 0x69, 0xc7, 0x51, 0x0f, 0xc4, - 0x00, 0x58, 0x98, 0x22, 0x84, 0xe1, 0xf3, 0xcd, 0xbc, 0x27, 0x42, 0xb2, 0xe4, 0x14, 0x2f, 0x56, - 0xd4, 0xc4, 0xb1, 0x25, 0xc5, 0xfd, 0x8b, 0x49, 0x58, 0x50, 0x2c, 0xe3, 0x9e, 0x10, 0xe3, 0xec, - 0x50, 0x0f, 0x9e, 0x51, 0x81, 0xfd, 0xe0, 0xa1, 0xa2, 0x37, 0xed, 0xba, 0xe1, 0x7f, 0x66, 0x2b, - 0xb3, 0xd7, 0xa3, 0xa1, 0x14, 0x3c, 0x04, 0x77, 0xf0, 0xa4, 0x80, 0x5f, 0xbc, 0x93, 0xc2, 0x56, - 0xb6, 0xa9, 0x13, 0x95, 0x6d, 0xe8, 0xb9, 0x32, 0xfd, 0x04, 0xe7, 0xca, 0xa0, 0xaf, 0x2f, 0xe7, - 0xf2, 0xf5, 0x7f, 0x53, 0x80, 0xa9, 0x1d, 0x16, 0xa8, 0x0a, 0xf0, 0xd9, 0x17, 0x13, 0xef, 0xc1, - 0x04, 0xef, 0xd1, 0x86, 0xf1, 0x79, 0x97, 0x47, 0xb3, 0x34, 0x33, 0xbc, 0x7a, 0x8f, 0x36, 0x6a, - 0xd3, 0xd2, 0x8d, 0xca, 0x5f, 0x58, 0x01, 0x59, 0x55, 0xb1, 0x62, 0x9e, 0x08, 0x3c, 0x84, 0x7c, - 0x7c, 0x55, 0xec, 0x53, 0x07, 0x2a, 0xa6, 0xe7, 0x57, 0xb6, 0xfc, 0x62, 0xc6, 0x37, 0xa4, 0xfc, - 0xf2, 0x57, 0xf1, 0x0c, 0xe4, 0xa2, 0xa1, 0x3f, 0x80, 0x85, 0x1e, 0x0b, 0xba, 0x81, 0xb4, 0xd4, - 0x9d, 0xa0, 0xe3, 0x35, 0x3c, 0x1a, 0x56, 0xf0, 0xae, 0x8e, 0x2c, 0xcf, 0x62, 0x3f, 0xac, 0x7d, - 0xcd, 0xc8, 0x5d, 0xd8, 0x49, 0xe3, 0xe2, 0x41, 0x51, 0xee, 0x7f, 0x39, 0x30, 0x9b, 0x58, 0x7b, - 0xd4, 0x00, 0x68, 0x04, 0x7e, 0xd3, 0x13, 0xd1, 0x2d, 0x69, 0xe5, 0xca, 0xda, 0x68, 0xab, 0xba, - 0x11, 0xf2, 0xc5, 0x4a, 0x17, 0x35, 0x71, 0x6c, 0xc1, 0xa2, 0xd7, 0xc2, 0x07, 0x0b, 0xc9, 0x20, - 0x46, 0x3f, 0x58, 0x78, 0x74, 0xb4, 0x3a, 0x63, 0xc6, 0x64, 0x3f, 0x60, 0xc8, 0x73, 0x75, 0xff, - 0x8f, 0x05, 0x28, 0x47, 0xf3, 0x7f, 0x0a, 0x66, 0xf4, 0x6e, 0xc2, 0x8c, 0x5e, 0xcb, 0xb9, 0x73, - 0xca, 0x90, 0xa2, 0x98, 0xc4, 0x32, 0xa6, 0x0f, 0x52, 0xc6, 0x94, 0x57, 0x25, 0x4e, 0x30, 0xa7, - 0x7f, 0xd5, 0x9b, 0xaf, 0xfb, 0x3e, 0x05, 0x83, 0xba, 0x9f, 0x34, 0xa8, 0xb5, 0x9c, 0xb3, 0x19, - 0x62, 0x52, 0x3f, 0x70, 0x60, 0x2e, 0x65, 0x04, 0xe8, 0x1b, 0x30, 0xa9, 0x4a, 0x36, 0x46, 0xbf, - 0x22, 0x46, 0x93, 0xe0, 0x2b, 0x1a, 0xda, 0x81, 0x45, 0xd2, 0x17, 0x41, 0xc4, 0x7b, 0xcb, 0x27, - 0xbb, 0x1d, 0xaa, 0xb3, 0xf6, 0xe9, 0xda, 0x2f, 0x19, 0x9e, 0xc5, 0xf5, 0x8c, 0x3e, 0x38, 0x93, - 0xd3, 0xfd, 0x49, 0x01, 0x50, 0xd4, 0x98, 0xa7, 0xd4, 0xf9, 0x01, 0x4c, 0xed, 0xe9, 0xa2, 0xcc, - 0x93, 0xd5, 0xaa, 0x6b, 0x15, 0xbb, 0x5c, 0x1f, 0x62, 0xa2, 0x6f, 0x9f, 0x8e, 0x1e, 0xc1, 0xa0, - 0x0e, 0xa1, 0x87, 0x00, 0x7b, 0x9e, 0xef, 0xf1, 0xf6, 0x98, 0xb7, 0x6a, 0xea, 0x34, 0xbf, 0x1d, - 0x21, 0x60, 0x0b, 0xcd, 0xfd, 0xb8, 0x60, 0xe9, 0xa7, 0x72, 0x97, 0x23, 0xed, 0xeb, 0x4b, 0xc9, - 0xc5, 0x2c, 0x0f, 0xde, 0x63, 0x58, 0x0b, 0x33, 0x71, 0x40, 0x58, 0x58, 0x52, 0xbd, 0x96, 0x73, - 0x59, 0x1e, 0x10, 0xe6, 0xc9, 0x8d, 0x8f, 0xb7, 0xf4, 0x01, 0x61, 0x1c, 0x2b, 0x48, 0xf4, 0x2d, - 0x39, 0x54, 0xda, 0x0b, 0x5d, 0x68, 0x6e, 0x9f, 0x20, 0x68, 0xcf, 0x9e, 0x1f, 0xed, 0x71, 0xac, - 0x01, 0xdd, 0x8f, 0xa7, 0x2c, 0x85, 0x37, 0x5e, 0xfb, 0x2e, 0xa0, 0x0e, 0xe1, 0xe2, 0x6d, 0xe2, - 0x37, 0xa5, 0x7a, 0xd2, 0x3d, 0x46, 0x79, 0xdb, 0x44, 0x68, 0xcb, 0x06, 0x05, 0x6d, 0x0f, 0xf4, - 0xc0, 0x19, 0x5c, 0xe8, 0x6a, 0xd2, 0x39, 0xaf, 0xa6, 0x9d, 0xf3, 0xf9, 0xd8, 0xda, 0xc6, 0x73, - 0xcf, 0xb6, 0xba, 0x4f, 0x9e, 0x81, 0xba, 0xff, 0x3e, 0x2c, 0xec, 0xa5, 0xef, 0xb5, 0xcc, 0x2d, - 0xf7, 0xb5, 0x31, 0xaf, 0xc5, 0x6a, 0x17, 0x8f, 0xe3, 0xcb, 0x90, 0xb8, 0x19, 0x0f, 0x0a, 0x42, - 0x41, 0xf8, 0x9e, 0x4b, 0x65, 0x9f, 0xba, 0xb0, 0x30, 0xb2, 0xc9, 0xa5, 0xf2, 0xd6, 0xf4, 0x4b, - 0x2e, 0x0d, 0x89, 0x13, 0x02, 0x52, 0x26, 0x58, 0x3a, 0x4d, 0x13, 0x44, 0x57, 0xa1, 0xd2, 0xe8, - 0x33, 0x46, 0x7d, 0x21, 0x87, 0xa3, 0x42, 0xd9, 0x62, 0x5c, 0x20, 0xde, 0x88, 0x49, 0xd8, 0xee, - 0x87, 0x7e, 0xe8, 0xc0, 0x45, 0xa9, 0xac, 0xb7, 0x3e, 0xa2, 0x8d, 0xbe, 0x5c, 0x95, 0xf0, 0x11, - 0xe7, 0x52, 0x45, 0xad, 0xc6, 0x88, 0xaf, 0xdb, 0xea, 0x59, 0x10, 0x71, 0x5c, 0x9e, 0x49, 0xc6, - 0xd9, 0x82, 0xd1, 0x87, 0xca, 0x75, 0x08, 0xaa, 0xd2, 0x9e, 0x27, 0x4f, 0xef, 0xcb, 0xc6, 0xed, - 0x08, 0xed, 0x76, 0x04, 0x75, 0xff, 0x32, 0xe1, 0xad, 0x46, 0x2b, 0x3a, 0x2c, 0x43, 0x81, 0xf0, - 0x74, 0x99, 0x78, 0x9d, 0xe3, 0x02, 0xe1, 0xe8, 0xdb, 0x30, 0xc9, 0xa8, 0x60, 0x87, 0xc6, 0xa9, - 0x5e, 0x1f, 0xc3, 0x81, 0x60, 0xc9, 0xaf, 0x87, 0xaa, 0x7e, 0x62, 0x8d, 0x68, 0xd5, 0x3a, 0x8a, - 0x67, 0x56, 0xeb, 0xf8, 0xb1, 0x63, 0x9d, 0x85, 0xd1, 0x60, 0xd0, 0xbb, 0x30, 0x25, 0xbc, 0x2e, - 0x0d, 0xfa, 0x22, 0x5f, 0x80, 0xb1, 0xd9, 0x67, 0xea, 0xee, 0x57, 0x9b, 0xfc, 0x7d, 0x0d, 0x81, - 0x43, 0x2c, 0x99, 0x75, 0x51, 0xc6, 0x02, 0x76, 0xbf, 0x2d, 0x5d, 0x58, 0xd0, 0xd1, 0xa7, 0xf8, - 0x6c, 0x9c, 0x75, 0xdd, 0x4a, 0x50, 0x71, 0xaa, 0xb7, 0xcb, 0x20, 0x8e, 0x97, 0xc3, 0xd7, 0xb1, - 0xe8, 0x03, 0x13, 0xd5, 0x39, 0x79, 0x5e, 0x64, 0x0e, 0xc0, 0x0c, 0x8b, 0xee, 0xdc, 0x9f, 0x38, - 0x70, 0x31, 0xb3, 0x77, 0x74, 0x2c, 0x15, 0xce, 0xf0, 0x58, 0x72, 0x4e, 0xfb, 0x58, 0x7a, 0x68, - 0x2d, 0x61, 0x38, 0x84, 0xd3, 0x7a, 0xd2, 0xfe, 0xa3, 0x02, 0xcc, 0x63, 0xda, 0x0b, 0x12, 0x15, - 0x9a, 0x9d, 0xf0, 0x8d, 0x5b, 0x8e, 0x90, 0x26, 0x75, 0x0f, 0x50, 0x9b, 0x4a, 0x3c, 0x6e, 0xfb, - 0x16, 0x4c, 0xaa, 0xe2, 0x89, 0x09, 0xc2, 0xae, 0xe5, 0xb8, 0xb7, 0x4d, 0xa0, 0x2a, 0x8b, 0xd3, - 0x55, 0x28, 0x0d, 0x28, 0x91, 0xd5, 0x95, 0xab, 0x31, 0xb8, 0x6b, 0x39, 0x2e, 0x6f, 0x07, 0x91, - 0x55, 0x33, 0xd6, 0x80, 0xee, 0x27, 0x05, 0xd0, 0xe1, 0xcf, 0x53, 0x48, 0x73, 0x7e, 0x2b, 0x91, - 0xe6, 0xac, 0x8d, 0xea, 0xc4, 0xe5, 0xf2, 0x0c, 0x4b, 0x71, 0xd2, 0xa1, 0xe9, 0xe5, 0x3c, 0xa0, - 0x8f, 0x4f, 0x6f, 0xfe, 0xc5, 0x81, 0xb2, 0xea, 0xf7, 0x14, 0x52, 0x9b, 0x9d, 0x64, 0x6a, 0xf3, - 0x72, 0x8e, 0x59, 0x0c, 0x49, 0x6b, 0x3e, 0x2e, 0x9a, 0xd1, 0x47, 0x81, 0x6f, 0x9b, 0xb0, 0xa6, - 0x09, 0xe9, 0x62, 0x0b, 0x94, 0x8d, 0x58, 0xd3, 0xd0, 0xef, 0xc2, 0x3c, 0xd3, 0xb7, 0xf8, 0xb4, - 0x79, 0x3b, 0x8a, 0xaf, 0x8a, 0xb9, 0xaf, 0xd9, 0xcd, 0x53, 0x80, 0xb8, 0xb4, 0x87, 0x53, 0xa8, - 0x78, 0x40, 0x8e, 0x8c, 0xb9, 0x7a, 0x69, 0x5f, 0x66, 0x62, 0x91, 0x6b, 0x63, 0x3a, 0x4e, 0x1d, - 0x73, 0x0d, 0x34, 0xe3, 0x41, 0x41, 0xa8, 0x0d, 0x33, 0xf6, 0x03, 0x21, 0xa3, 0x4b, 0x57, 0xf2, - 0xbf, 0x44, 0xd2, 0x57, 0x39, 0x76, 0x0b, 0x4e, 0x20, 0xbb, 0x47, 0x25, 0xa8, 0x58, 0xca, 0x97, - 0x2a, 0x97, 0xcc, 0x9e, 0x4d, 0xb9, 0x24, 0x3b, 0xba, 0xaf, 0x8c, 0x15, 0xdd, 0x5f, 0x4e, 0x46, - 0xf7, 0x5f, 0x4f, 0x47, 0xf7, 0xa0, 0x66, 0x97, 0x88, 0xec, 0x39, 0x9c, 0x37, 0x61, 0x6e, 0xf8, - 0xd2, 0x2b, 0x57, 0xbe, 0x34, 0x18, 0x4c, 0x23, 0x79, 0x22, 0xdf, 0x4e, 0x40, 0xe2, 0x94, 0x08, - 0x79, 0xa2, 0x9b, 0x96, 0x7a, 0xbf, 0xdb, 0x25, 0xec, 0x70, 0x69, 0x46, 0x0d, 0x38, 0x3a, 0xd1, - 0x6f, 0x27, 0xa8, 0x38, 0xd5, 0x1b, 0xed, 0x40, 0x49, 0x47, 0xc9, 0xe6, 0xf5, 0xd0, 0x2b, 0x79, - 0x02, 0x70, 0x1d, 0xd1, 0xe8, 0xdf, 0xd8, 0xe0, 0xd8, 0x09, 0x4e, 0xf9, 0x84, 0x04, 0xe7, 0x2e, - 0xa0, 0x60, 0x57, 0xc5, 0x4e, 0xcd, 0x3b, 0xfa, 0xdb, 0x2f, 0xa9, 0x95, 0x25, 0x15, 0x3d, 0x47, - 0x1b, 0x76, 0x6f, 0xa0, 0x07, 0xce, 0xe0, 0x92, 0x56, 0x6d, 0x42, 0xeb, 0xc8, 0x14, 0x4c, 0x32, - 0x93, 0x37, 0x24, 0x8c, 0xf3, 0xa6, 0x45, 0x69, 0xd5, 0x1b, 0x29, 0x54, 0x3c, 0x20, 0x07, 0x7d, - 0x17, 0x66, 0xa5, 0x0a, 0xc5, 0x82, 0xe1, 0x09, 0x05, 0x2f, 0x1c, 0x1f, 0xad, 0xce, 0x6e, 0xdb, - 0x90, 0x38, 0x29, 0xc1, 0xfd, 0xf3, 0x22, 0x64, 0x07, 0xf6, 0xf1, 0xc3, 0x57, 0xe7, 0x31, 0x0f, - 0x5f, 0xdf, 0x83, 0x32, 0x17, 0x84, 0xe9, 0x47, 0xbe, 0x85, 0xf1, 0x1e, 0xf9, 0xd6, 0x43, 0x00, - 0x1c, 0x63, 0xa5, 0xb2, 0xac, 0xe2, 0xa9, 0x66, 0x59, 0x57, 0x00, 0x54, 0x3c, 0xba, 0x11, 0xf4, - 0xcd, 0xbd, 0xca, 0x6c, 0xec, 0x13, 0x6e, 0x45, 0x14, 0x6c, 0xf5, 0x42, 0xd7, 0xa3, 0x83, 0x53, - 0x5f, 0xa4, 0x5c, 0x1a, 0xb8, 0x08, 0x4e, 0xe7, 0xe9, 0x19, 0x9f, 0x40, 0x9d, 0xf0, 0x70, 0xc4, - 0xfd, 0xbf, 0x02, 0x24, 0x9c, 0x21, 0xfa, 0x81, 0x03, 0x0b, 0x24, 0xf5, 0x15, 0x59, 0x18, 0x4b, - 0xfe, 0x46, 0xbe, 0x4f, 0xfb, 0x06, 0x3e, 0x42, 0x8b, 0x4b, 0xd7, 0xe9, 0x2e, 0x1c, 0x0f, 0x0a, - 0x45, 0x7f, 0xea, 0xc0, 0x05, 0x32, 0xf8, 0x99, 0xa0, 0xd9, 0xf4, 0x37, 0xc7, 0xfe, 0xce, 0xb0, - 0xf6, 0xec, 0xf1, 0xd1, 0x6a, 0xd6, 0x07, 0x94, 0x38, 0x4b, 0x1c, 0xfa, 0x0e, 0x4c, 0x10, 0xd6, - 0x0a, 0xcb, 0x3c, 0xf9, 0xc5, 0x86, 0x5f, 0x7f, 0xc6, 0xd1, 0xd1, 0x3a, 0x6b, 0x71, 0xac, 0x40, - 0xdd, 0x9f, 0x16, 0x61, 0x3e, 0xfd, 0x50, 0xd6, 0xbc, 0x2d, 0x9a, 0xc8, 0x7c, 0x5b, 0x24, 0x6d, - 0xa4, 0x21, 0xa2, 0x87, 0x3e, 0xb1, 0x8d, 0xc8, 0x46, 0xac, 0x69, 0x91, 0x8d, 0x48, 0xbd, 0x34, - 0x05, 0x98, 0xf1, 0x6c, 0x44, 0xfe, 0x89, 0x63, 0x2c, 0x74, 0x3d, 0x79, 0xb6, 0xb8, 0xe9, 0xb3, - 0x65, 0xc1, 0x9e, 0xcb, 0xb8, 0xc5, 0xa3, 0x2e, 0x54, 0xac, 0x7d, 0x30, 0x96, 0x78, 0x23, 0xf7, - 0xba, 0xc7, 0x6a, 0x37, 0xa7, 0x3f, 0x21, 0x8d, 0x29, 0x36, 0x7e, 0x6c, 0xf7, 0x6a, 0xb5, 0x9e, - 0xa8, 0xba, 0xa2, 0x96, 0xcb, 0x42, 0x73, 0xf7, 0x61, 0x36, 0xf1, 0xde, 0x4d, 0x0a, 0x0b, 0xdf, - 0xf8, 0x8d, 0xff, 0x4d, 0xe5, 0x83, 0x08, 0x01, 0x5b, 0x68, 0xea, 0x4e, 0xe4, 0x3d, 0xc2, 0x68, - 0x3b, 0xe8, 0x73, 0xfa, 0x55, 0xbd, 0x13, 0x89, 0x06, 0x78, 0xda, 0x77, 0x22, 0x31, 0xf0, 0xc9, - 0x77, 0x22, 0x51, 0xdf, 0xaf, 0xec, 0x9d, 0x48, 0x34, 0xc2, 0x21, 0xc9, 0xc3, 0xff, 0x16, 0xac, - 0x59, 0x24, 0x13, 0x88, 0xc2, 0x63, 0x12, 0x88, 0xf7, 0x61, 0xda, 0xf3, 0x05, 0x65, 0x07, 0xa4, - 0x63, 0xaa, 0x4e, 0x79, 0xab, 0x33, 0xd1, 0x54, 0xb7, 0x0c, 0x0e, 0x8e, 0x10, 0x51, 0x07, 0x2e, - 0x86, 0xd5, 0x52, 0x46, 0x49, 0x7c, 0x5b, 0x63, 0x5e, 0x3c, 0xbc, 0x11, 0x96, 0xf5, 0x6e, 0x67, - 0x75, 0x7a, 0x34, 0x8c, 0x80, 0xb3, 0x41, 0x11, 0x87, 0x59, 0x6e, 0x65, 0xce, 0xe1, 0x21, 0x35, - 0x62, 0xa5, 0x39, 0x5d, 0x6c, 0xb0, 0x9e, 0x49, 0xd8, 0xa0, 0x38, 0x29, 0xc3, 0xfd, 0x8f, 0x22, - 0xcc, 0xa5, 0x34, 0x2d, 0x95, 0x21, 0x94, 0x9f, 0x66, 0x86, 0x50, 0x1a, 0x2b, 0x43, 0xc8, 0x0e, - 0x5e, 0x27, 0xc6, 0x0a, 0x5e, 0xdf, 0xd2, 0x01, 0xa4, 0xd9, 0xb9, 0xad, 0x4d, 0xf3, 0xaa, 0x34, - 0x5a, 0xcd, 0x6d, 0x9b, 0x88, 0x93, 0x7d, 0xd5, 0x09, 0xdf, 0x1c, 0xfc, 0x84, 0xd2, 0x44, 0xbf, - 0x6f, 0xe6, 0x7d, 0x16, 0x14, 0x01, 0xe8, 0x13, 0x3e, 0x83, 0x80, 0xb3, 0xc4, 0xd5, 0xee, 0x7e, - 0xf6, 0xe5, 0xca, 0xb9, 0xcf, 0xbf, 0x5c, 0x39, 0xf7, 0xc5, 0x97, 0x2b, 0xe7, 0xbe, 0x7f, 0xbc, - 0xe2, 0x7c, 0x76, 0xbc, 0xe2, 0x7c, 0x7e, 0xbc, 0xe2, 0x7c, 0x71, 0xbc, 0xe2, 0xfc, 0xf7, 0xf1, - 0x8a, 0xf3, 0xc3, 0x9f, 0xad, 0x9c, 0x7b, 0xf8, 0xfc, 0x28, 0xff, 0x47, 0xe3, 0xff, 0x03, 0x00, - 0x00, 0xff, 0xff, 0x6a, 0xff, 0x8d, 0xf8, 0x6e, 0x43, 0x00, 0x00, + // 3936 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x5c, 0xcd, 0x6f, 0x23, 0x47, + 0x76, 0x9f, 0x26, 0x45, 0x4a, 0x7c, 0x94, 0x46, 0x52, 0x8d, 0xc6, 0xd6, 0x6a, 0x63, 0x69, 0xd2, + 0x6b, 0x18, 0x76, 0x6c, 0x53, 0x99, 0xb1, 0xc7, 0x33, 0x1e, 0x27, 0x13, 0x90, 0xd4, 0x7c, 0x68, + 0x56, 0xeb, 0x51, 0x8a, 0xe3, 0xf1, 0xee, 0xd8, 0x86, 0xd3, 0x22, 0x4b, 0x64, 0xaf, 0xc8, 0x6e, + 0xba, 0xaa, 0xa8, 0xb5, 0x92, 0x20, 0xd9, 0x7c, 0x01, 0x8b, 0x7c, 0x61, 0x0f, 0x06, 0xbc, 0x01, + 0x12, 0x20, 0x48, 0x8e, 0x8b, 0xe4, 0x1f, 0xc8, 0xc1, 0x87, 0x5c, 0x8c, 0x60, 0x11, 0x18, 0x49, + 0x80, 0xf8, 0xb0, 0x50, 0x62, 0x19, 0xc8, 0x31, 0xb7, 0x5c, 0x06, 0x08, 0x10, 0xd4, 0x47, 0x77, + 0x57, 0x37, 0x9b, 0x23, 0x36, 0x47, 0x1a, 0x0c, 0x72, 0xa3, 0xea, 0xd5, 0xfb, 0xbd, 0xaa, 0x57, + 0xf5, 0x5e, 0xbd, 0xf7, 0xaa, 0x5a, 0xf0, 0x7a, 0xdb, 0xe5, 0x9d, 0xc1, 0x4e, 0xa5, 0xe9, 0xf7, + 0xd6, 0x9d, 0xbd, 0x81, 0xcb, 0x0f, 0xd6, 0xf7, 0x1c, 0xda, 0xf6, 0xd7, 0x9d, 0xbe, 0xbb, 0xbe, + 0x7f, 0xd1, 0xe9, 0xf6, 0x3b, 0xce, 0xc5, 0xf5, 0x36, 0xf1, 0x08, 0x75, 0x38, 0x69, 0x55, 0xfa, + 0xd4, 0xe7, 0x3e, 0x7a, 0x3e, 0xe2, 0xaa, 0x28, 0xae, 0x8a, 0xe4, 0xaa, 0x38, 0x7d, 0xb7, 0x12, + 0x70, 0xad, 0xbc, 0x6a, 0x60, 0xb7, 0xfd, 0xb6, 0xbf, 0x2e, 0x99, 0x77, 0x06, 0xbb, 0xf2, 0x2f, + 0xf9, 0x87, 0xfc, 0xa5, 0x40, 0x57, 0x6e, 0xef, 0x5d, 0x65, 0x15, 0x57, 0x4a, 0x26, 0x1f, 0x73, + 0xe2, 0x31, 0xd7, 0xf7, 0xd8, 0xab, 0x4e, 0xdf, 0x65, 0x84, 0xee, 0x13, 0xba, 0xde, 0xdf, 0x6b, + 0x0b, 0x1a, 0x8b, 0x77, 0x58, 0xdf, 0x1f, 0x1a, 0xde, 0xca, 0xeb, 0x11, 0x52, 0xcf, 0x69, 0x76, + 0x5c, 0x8f, 0xd0, 0x83, 0x88, 0xbd, 0x47, 0xb8, 0x93, 0xc6, 0xb5, 0x3e, 0x8a, 0x8b, 0x0e, 0x3c, + 0xee, 0xf6, 0xc8, 0x10, 0xc3, 0x1b, 0xc7, 0x31, 0xb0, 0x66, 0x87, 0xf4, 0x9c, 0x24, 0x9f, 0xfd, + 0x3e, 0x9c, 0xab, 0x7a, 0x4e, 0xf7, 0x80, 0xb9, 0x0c, 0x0f, 0xbc, 0x2a, 0x6d, 0x0f, 0x7a, 0xc4, + 0xe3, 0xe8, 0x02, 0x4c, 0x79, 0x4e, 0x8f, 0x2c, 0x5b, 0x17, 0xac, 0x17, 0x4b, 0xb5, 0xd9, 0xcf, + 0x0f, 0xd7, 0xce, 0x1c, 0x1d, 0xae, 0x4d, 0xbd, 0xed, 0xf4, 0x08, 0x96, 0x14, 0xf4, 0x2d, 0x28, + 0xec, 0x3b, 0xdd, 0x01, 0x59, 0xce, 0xc9, 0x2e, 0x73, 0xba, 0x4b, 0xe1, 0xbe, 0x68, 0xc4, 0x8a, + 0x66, 0xff, 0x41, 0x3e, 0x06, 0xff, 0x1d, 0xc2, 0x9d, 0x96, 0xc3, 0x1d, 0xd4, 0x83, 0x62, 0xd7, + 0xd9, 0x21, 0x5d, 0xb6, 0x6c, 0x5d, 0xc8, 0xbf, 0x58, 0xbe, 0x74, 0xa3, 0x32, 0xce, 0x22, 0x56, + 0x52, 0xa0, 0x2a, 0x5b, 0x12, 0xe7, 0x86, 0xc7, 0xe9, 0x41, 0xed, 0xac, 0x1e, 0x44, 0x51, 0x35, + 0x62, 0x2d, 0x04, 0xfd, 0x9e, 0x05, 0x65, 0xc7, 0xf3, 0x7c, 0xee, 0x70, 0xb1, 0x4c, 0xcb, 0x39, + 0x29, 0xf4, 0xce, 0xe4, 0x42, 0xab, 0x11, 0x98, 0x92, 0x7c, 0x4e, 0x4b, 0x2e, 0x1b, 0x14, 0x6c, + 0xca, 0x5c, 0x79, 0x13, 0xca, 0xc6, 0x50, 0xd1, 0x02, 0xe4, 0xf7, 0xc8, 0x81, 0xd2, 0x2f, 0x16, + 0x3f, 0xd1, 0x52, 0x4c, 0xa1, 0x5a, 0x83, 0xd7, 0x72, 0x57, 0xad, 0x95, 0xeb, 0xb0, 0x90, 0x14, + 0x98, 0x85, 0xdf, 0xfe, 0x73, 0x0b, 0x96, 0x8c, 0x59, 0x60, 0xb2, 0x4b, 0x28, 0xf1, 0x9a, 0x04, + 0xad, 0x43, 0x49, 0xac, 0x25, 0xeb, 0x3b, 0xcd, 0x60, 0xa9, 0x17, 0xf5, 0x44, 0x4a, 0x6f, 0x07, + 0x04, 0x1c, 0xf5, 0x09, 0xb7, 0x45, 0xee, 0x51, 0xdb, 0xa2, 0xdf, 0x71, 0x18, 0x59, 0xce, 0xc7, + 0xb7, 0xc5, 0xb6, 0x68, 0xc4, 0x8a, 0x66, 0xff, 0x2a, 0x7c, 0x23, 0x18, 0xcf, 0x3d, 0xd2, 0xeb, + 0x77, 0x1d, 0x4e, 0xa2, 0x41, 0x1d, 0xbb, 0xf5, 0xec, 0x3d, 0x98, 0xab, 0xf6, 0xfb, 0xd4, 0xdf, + 0x27, 0xad, 0x06, 0x77, 0xda, 0x04, 0x3d, 0x00, 0x70, 0x74, 0x43, 0x95, 0x4b, 0xc6, 0xf2, 0xa5, + 0x5f, 0xaa, 0x28, 0x8b, 0xa8, 0x98, 0x16, 0x51, 0xe9, 0xef, 0xb5, 0x45, 0x03, 0xab, 0x08, 0xc3, + 0xab, 0xec, 0x5f, 0xac, 0xdc, 0x73, 0x7b, 0xa4, 0x76, 0xf6, 0xe8, 0x70, 0x0d, 0xaa, 0x21, 0x02, + 0x36, 0xd0, 0xec, 0xdf, 0xb7, 0xe0, 0x7c, 0x95, 0xb6, 0xfd, 0xfa, 0x46, 0xb5, 0xdf, 0xbf, 0x4d, + 0x9c, 0x2e, 0xef, 0x34, 0xb8, 0xc3, 0x07, 0x0c, 0x5d, 0x87, 0x22, 0x93, 0xbf, 0xf4, 0x50, 0x5f, + 0x08, 0x76, 0x9f, 0xa2, 0x3f, 0x3c, 0x5c, 0x5b, 0x4a, 0x61, 0x24, 0x58, 0x73, 0xa1, 0x97, 0x60, + 0xba, 0x47, 0x18, 0x73, 0xda, 0x81, 0x3e, 0xe7, 0x35, 0xc0, 0xf4, 0x77, 0x54, 0x33, 0x0e, 0xe8, + 0xf6, 0x3f, 0xe5, 0x60, 0x3e, 0xc4, 0xd2, 0xe2, 0x4f, 0x61, 0xf1, 0x06, 0x30, 0xdb, 0x31, 0x66, + 0x28, 0xd7, 0xb0, 0x7c, 0xe9, 0xad, 0x31, 0xed, 0x24, 0x4d, 0x49, 0xb5, 0x25, 0x2d, 0x66, 0xd6, + 0x6c, 0xc5, 0x31, 0x31, 0xa8, 0x07, 0xc0, 0x0e, 0xbc, 0xa6, 0x16, 0x3a, 0x25, 0x85, 0xbe, 0x99, + 0x51, 0x68, 0x23, 0x04, 0xa8, 0x21, 0x2d, 0x12, 0xa2, 0x36, 0x6c, 0x08, 0xb0, 0xff, 0xde, 0x82, + 0x73, 0x29, 0x7c, 0xe8, 0x57, 0x12, 0xeb, 0xf9, 0xfc, 0xd0, 0x7a, 0xa2, 0x21, 0xb6, 0x68, 0x35, + 0x5f, 0x81, 0x19, 0x4a, 0xf6, 0x5d, 0x71, 0x0e, 0x68, 0x0d, 0x2f, 0x68, 0xfe, 0x19, 0xac, 0xdb, + 0x71, 0xd8, 0x03, 0xbd, 0x0c, 0xa5, 0xe0, 0xb7, 0x50, 0x73, 0x5e, 0x98, 0x8a, 0x58, 0xb8, 0xa0, + 0x2b, 0xc3, 0x11, 0xdd, 0xfe, 0x5d, 0x28, 0xd4, 0x3b, 0x0e, 0xe5, 0x62, 0xc7, 0x50, 0xd2, 0xf7, + 0xdf, 0xc1, 0x5b, 0x7a, 0x88, 0xe1, 0x8e, 0xc1, 0xaa, 0x19, 0x07, 0xf4, 0x31, 0x16, 0xfb, 0x25, + 0x98, 0xde, 0x27, 0x54, 0x8e, 0x37, 0x1f, 0x07, 0xbb, 0xaf, 0x9a, 0x71, 0x40, 0xb7, 0xff, 0xd5, + 0x82, 0x25, 0x39, 0x82, 0x0d, 0x97, 0x35, 0xfd, 0x7d, 0x42, 0x0f, 0x30, 0x61, 0x83, 0xee, 0x09, + 0x0f, 0x68, 0x03, 0x16, 0x18, 0xe9, 0xed, 0x13, 0x5a, 0xf7, 0x3d, 0xc6, 0xa9, 0xe3, 0x7a, 0x5c, + 0x8f, 0x6c, 0x59, 0xf7, 0x5e, 0x68, 0x24, 0xe8, 0x78, 0x88, 0x03, 0xbd, 0x08, 0x33, 0x7a, 0xd8, + 0x62, 0x2b, 0x09, 0xc5, 0xce, 0x8a, 0x35, 0xd0, 0x73, 0x62, 0x38, 0xa4, 0xda, 0xff, 0x65, 0xc1, + 0xa2, 0x9c, 0x55, 0x63, 0xb0, 0xc3, 0x9a, 0xd4, 0xed, 0x0b, 0xf7, 0xfa, 0x34, 0x4e, 0xe9, 0x3a, + 0x9c, 0x6d, 0x05, 0x8a, 0xdf, 0x72, 0x7b, 0x2e, 0x97, 0x36, 0x52, 0xa8, 0x3d, 0xa3, 0x31, 0xce, + 0x6e, 0xc4, 0xa8, 0x38, 0xd1, 0xdb, 0x7e, 0x0f, 0x66, 0xeb, 0x03, 0x4a, 0x89, 0xc7, 0x95, 0xbb, + 0xfc, 0x36, 0x14, 0x98, 0xeb, 0x69, 0xaf, 0x91, 0xcd, 0x53, 0x96, 0x84, 0x2f, 0x6f, 0x08, 0x66, + 0xac, 0x30, 0xec, 0xbf, 0xcc, 0xc3, 0xb9, 0x40, 0x3e, 0x69, 0x55, 0x29, 0x77, 0x77, 0x9d, 0x26, + 0x67, 0xa8, 0x05, 0xb3, 0xad, 0xa8, 0x99, 0x6b, 0xb3, 0xce, 0x22, 0x2b, 0x74, 0x1d, 0x06, 0x3c, + 0xc7, 0x31, 0x54, 0xf4, 0x2e, 0xe4, 0xdb, 0x2e, 0xd7, 0x51, 0xc4, 0xd5, 0xf1, 0x7c, 0xc6, 0x2d, + 0x37, 0xb9, 0x8f, 0x6b, 0x65, 0x2d, 0x2a, 0x7f, 0xcb, 0xe5, 0x58, 0x20, 0xa2, 0x1d, 0x28, 0xba, + 0x3d, 0xa7, 0x4d, 0x82, 0x60, 0xe1, 0xda, 0x78, 0xd8, 0x9b, 0x82, 0x27, 0x89, 0x1e, 0x86, 0x25, + 0x92, 0xca, 0xb0, 0x46, 0x16, 0x32, 0x9a, 0x62, 0xff, 0x29, 0x0f, 0x30, 0xb6, 0x8c, 0x34, 0x4b, + 0x8c, 0x64, 0x48, 0x2a, 0xc3, 0x1a, 0xd9, 0xfe, 0x32, 0x07, 0x0b, 0x91, 0xfe, 0xea, 0x7e, 0xaf, + 0xe7, 0x72, 0xb4, 0x02, 0x39, 0xb7, 0xa5, 0xb7, 0x37, 0x68, 0xc6, 0xdc, 0xe6, 0x06, 0xce, 0xb9, + 0x2d, 0xf4, 0x02, 0x14, 0x77, 0xa8, 0xe3, 0x35, 0x3b, 0x7a, 0x5b, 0x87, 0xc0, 0x35, 0xd9, 0x8a, + 0x35, 0x15, 0x3d, 0x07, 0x79, 0xee, 0xb4, 0xf5, 0x6e, 0x0e, 0xf5, 0x77, 0xcf, 0x69, 0x63, 0xd1, + 0x2e, 0xcc, 0x88, 0x0d, 0x76, 0xbe, 0x4f, 0x9a, 0x6a, 0xe5, 0x0d, 0x33, 0x6a, 0xa8, 0x66, 0x1c, + 0xd0, 0x85, 0x44, 0x67, 0xc0, 0x3b, 0x3e, 0x5d, 0x2e, 0xc4, 0x25, 0x56, 0x65, 0x2b, 0xd6, 0x54, + 0x71, 0xe0, 0x35, 0xe5, 0xf8, 0x39, 0xa1, 0xcb, 0xc5, 0xf8, 0x81, 0x57, 0x0f, 0x08, 0x38, 0xea, + 0x83, 0x3e, 0x80, 0x72, 0x93, 0x12, 0x87, 0xfb, 0x74, 0xc3, 0xe1, 0x64, 0x79, 0x3a, 0xf3, 0x0e, + 0x9c, 0x17, 0x11, 0x5d, 0x3d, 0x82, 0xc0, 0x26, 0x9e, 0xfd, 0xdf, 0x16, 0x2c, 0x47, 0xaa, 0x95, + 0x6b, 0x1b, 0x45, 0x31, 0x5a, 0x3d, 0xd6, 0x08, 0xf5, 0xbc, 0x00, 0xc5, 0x96, 0xdb, 0x26, 0x8c, + 0x27, 0xb5, 0xbc, 0x21, 0x5b, 0xb1, 0xa6, 0xa2, 0x4b, 0x00, 0x6d, 0x97, 0x6b, 0xcf, 0xa3, 0x95, + 0x1d, 0x9e, 0x6f, 0xb7, 0x42, 0x0a, 0x36, 0x7a, 0xa1, 0x77, 0xa1, 0x24, 0x87, 0x39, 0xa1, 0xd9, + 0xc9, 0x73, 0xa8, 0x1e, 0x00, 0xe0, 0x08, 0xcb, 0xfe, 0x62, 0x0a, 0xa6, 0x6f, 0x52, 0xe2, 0xb6, + 0x3b, 0x1c, 0xfd, 0x06, 0xcc, 0xf4, 0x74, 0x34, 0xac, 0xdd, 0xc8, 0x2f, 0x8f, 0x27, 0xe3, 0xae, + 0x5c, 0x74, 0x11, 0x49, 0x47, 0x13, 0x89, 0xda, 0x70, 0x88, 0x2a, 0x22, 0x49, 0xa7, 0xeb, 0x3a, + 0x4c, 0xae, 0x9b, 0x11, 0x49, 0x56, 0x45, 0x23, 0x56, 0x34, 0xf4, 0x1e, 0x14, 0x7d, 0xea, 0xb6, + 0x5d, 0x6f, 0xb9, 0x24, 0x07, 0xf1, 0xda, 0x78, 0x26, 0xa4, 0x67, 0x71, 0x57, 0xb2, 0x46, 0xca, + 0x57, 0x7f, 0x63, 0x0d, 0x89, 0x1e, 0xc0, 0xb4, 0xda, 0x4c, 0x81, 0x81, 0xae, 0x8f, 0xed, 0x60, + 0xd4, 0x7e, 0x8c, 0x36, 0xbd, 0xfa, 0x9b, 0xe1, 0x00, 0x10, 0x35, 0x42, 0xff, 0x32, 0x25, 0xa1, + 0x5f, 0xce, 0xe0, 0x5f, 0x46, 0x3a, 0x94, 0x46, 0xe8, 0x50, 0x0a, 0x59, 0x40, 0xa5, 0xcb, 0x18, + 0xe5, 0x41, 0x84, 0x8a, 0x75, 0x58, 0x54, 0x9c, 0x40, 0xc5, 0x3a, 0x26, 0x3b, 0x1b, 0x8f, 0xa5, + 0x82, 0xa8, 0xc9, 0xfe, 0x24, 0x0f, 0x8b, 0xba, 0x67, 0xdd, 0xef, 0x76, 0x49, 0x53, 0x9e, 0xc1, + 0xca, 0x3f, 0xe5, 0x53, 0xfd, 0x93, 0x0b, 0x05, 0x97, 0x93, 0x5e, 0x90, 0x39, 0xd6, 0x32, 0x8d, + 0x26, 0x92, 0x51, 0xd9, 0x14, 0x20, 0x2a, 0x79, 0x0b, 0x57, 0x49, 0xf7, 0xc2, 0x4a, 0x02, 0xfa, + 0x23, 0x0b, 0xce, 0xed, 0x13, 0xea, 0xee, 0xba, 0x4d, 0x99, 0x7a, 0xdd, 0x76, 0x19, 0xf7, 0xe9, + 0x81, 0x3e, 0x11, 0xde, 0x18, 0x4f, 0xf2, 0x7d, 0x03, 0x60, 0xd3, 0xdb, 0xf5, 0x6b, 0xdf, 0xd4, + 0xd2, 0xce, 0xdd, 0x1f, 0x86, 0xc6, 0x69, 0xf2, 0x56, 0xfa, 0x00, 0xd1, 0x68, 0x53, 0x32, 0xbf, + 0x2d, 0x33, 0xf3, 0x1b, 0x7b, 0x60, 0xc1, 0x64, 0x03, 0x97, 0x65, 0x66, 0x8c, 0x9f, 0x59, 0x50, + 0xd6, 0xf4, 0x2d, 0x97, 0x71, 0xf4, 0xfe, 0x90, 0xb5, 0x57, 0xc6, 0xb3, 0x76, 0xc1, 0x2d, 0x6d, + 0x3d, 0x0c, 0x86, 0x83, 0x16, 0xc3, 0xd2, 0x71, 0xb0, 0xa4, 0x4a, 0xb1, 0xaf, 0x66, 0x1a, 0x7f, + 0xe4, 0x18, 0xa4, 0x8e, 0xf4, 0xda, 0xd9, 0x14, 0xe6, 0x62, 0x46, 0x8e, 0x2e, 0xc3, 0xd4, 0x9e, + 0xeb, 0x05, 0xa7, 0xde, 0x2f, 0x06, 0xc1, 0xda, 0xb7, 0x5d, 0xaf, 0xf5, 0xf0, 0x70, 0x6d, 0x31, + 0xd6, 0x59, 0x34, 0x62, 0xd9, 0xfd, 0xf8, 0x18, 0xef, 0xda, 0xcc, 0x4f, 0xfe, 0x7a, 0xed, 0xcc, + 0x0f, 0x7f, 0x7e, 0xe1, 0x8c, 0xfd, 0x69, 0x1e, 0x16, 0x92, 0x5a, 0x1d, 0xa3, 0x92, 0x12, 0xf9, + 0xb0, 0x99, 0x53, 0xf5, 0x61, 0xb9, 0xd3, 0xf3, 0x61, 0xf9, 0xd3, 0xf0, 0x61, 0x53, 0x27, 0xe6, + 0xc3, 0xec, 0x7f, 0xb6, 0xe0, 0x6c, 0xb8, 0x32, 0x1f, 0x0d, 0xc4, 0xc9, 0x1a, 0x69, 0xdd, 0x3a, + 0x79, 0xad, 0x7f, 0x08, 0xd3, 0xcc, 0x1f, 0xd0, 0xa6, 0x0c, 0x1f, 0x05, 0xfa, 0xeb, 0xd9, 0x9c, + 0xa6, 0xe2, 0x35, 0x62, 0x26, 0xd5, 0x80, 0x03, 0x54, 0x73, 0x42, 0x9a, 0xa6, 0x42, 0x0a, 0x2a, + 0x02, 0x2e, 0x31, 0xa1, 0x19, 0x33, 0xa4, 0x10, 0xad, 0x58, 0x53, 0x91, 0x2d, 0xfd, 0x79, 0x10, + 0xd9, 0x96, 0x6a, 0xa0, 0xdd, 0xb2, 0x5c, 0x04, 0x45, 0x41, 0x7d, 0x58, 0xa0, 0xe4, 0xa3, 0x81, + 0x4b, 0x49, 0xab, 0xe1, 0x3b, 0x7b, 0x22, 0x2e, 0xd0, 0xc5, 0x80, 0x31, 0xed, 0x7e, 0x63, 0x40, + 0xa5, 0x0b, 0xab, 0x2d, 0x89, 0x1c, 0x07, 0x27, 0xb0, 0xf0, 0x10, 0xba, 0xfd, 0x1f, 0x85, 0xd0, + 0x60, 0x75, 0x3a, 0xfe, 0x5b, 0x50, 0x6e, 0xaa, 0xac, 0xa5, 0x7b, 0xb0, 0xe9, 0xe9, 0x2d, 0xb6, + 0x31, 0xc1, 0xe1, 0x53, 0xa9, 0x47, 0x30, 0x89, 0x6a, 0x9d, 0x41, 0xc1, 0xa6, 0x34, 0xf4, 0x03, + 0x00, 0xe5, 0x89, 0x49, 0x6b, 0xd3, 0xd3, 0x47, 0x4d, 0x7d, 0x12, 0xd9, 0xf7, 0x43, 0x14, 0x25, + 0x3a, 0x8c, 0x79, 0x22, 0x02, 0x36, 0x44, 0x89, 0x59, 0x07, 0xc5, 0xa7, 0x9b, 0x3e, 0xd5, 0x36, + 0x3b, 0xd1, 0xac, 0xab, 0x11, 0x4c, 0xb2, 0x46, 0x19, 0x51, 0xb0, 0x29, 0x6d, 0x85, 0xc2, 0x42, + 0x52, 0x57, 0x29, 0xc7, 0xcd, 0xed, 0xf8, 0x71, 0x73, 0x69, 0x4c, 0x03, 0x35, 0x32, 0x50, 0xb3, + 0xb8, 0x49, 0x61, 0x3e, 0xa1, 0xa3, 0x14, 0x91, 0x9b, 0x71, 0x91, 0xaf, 0x65, 0x39, 0x7a, 0x75, + 0x91, 0xd0, 0x94, 0xc9, 0x60, 0x21, 0xa9, 0x9d, 0x13, 0x13, 0x1a, 0xab, 0x4c, 0x9a, 0x67, 0xea, + 0x5f, 0xe5, 0xa0, 0x14, 0x7a, 0xd5, 0x2c, 0x65, 0x06, 0x15, 0x0d, 0xe5, 0x8e, 0xc9, 0xd6, 0xf2, + 0xe3, 0x64, 0x6b, 0x53, 0xa3, 0xb3, 0xb5, 0xa0, 0x14, 0x59, 0x7c, 0x74, 0x29, 0xd2, 0xc8, 0xd6, + 0xa6, 0xc7, 0xcf, 0xd6, 0x66, 0x8e, 0xcf, 0xd6, 0xec, 0xbf, 0xb1, 0x00, 0x0d, 0xa7, 0xe6, 0x59, + 0x14, 0xe5, 0x24, 0xcf, 0xba, 0x31, 0x23, 0xa1, 0x64, 0x7e, 0x3c, 0xfa, 0xc8, 0xb3, 0x3f, 0x2b, + 0xc0, 0xfc, 0x2d, 0x77, 0xe2, 0x8a, 0x11, 0x87, 0x67, 0x15, 0x52, 0x83, 0xe8, 0x38, 0xb4, 0xc1, + 0xa9, 0xc3, 0x49, 0xfb, 0x40, 0xaf, 0xef, 0x35, 0xcd, 0xfa, 0x6c, 0x3d, 0xbd, 0xdb, 0xc3, 0xd1, + 0x24, 0x3c, 0x0a, 0x7a, 0xec, 0x4d, 0xf2, 0x16, 0xcc, 0x31, 0x4e, 0xdd, 0x26, 0x57, 0x35, 0x29, + 0xb6, 0x5c, 0x96, 0x07, 0xc9, 0x79, 0xdd, 0x7d, 0xae, 0x61, 0x12, 0x71, 0xbc, 0x6f, 0x6a, 0xa9, + 0x6b, 0x2a, 0x73, 0xa9, 0x6b, 0x1d, 0x4a, 0x4e, 0xb7, 0xeb, 0xff, 0xe0, 0x9e, 0xd3, 0x66, 0xba, + 0x1c, 0x10, 0xee, 0x9a, 0x6a, 0x40, 0xc0, 0x51, 0x1f, 0x54, 0x01, 0x70, 0xdb, 0x9e, 0x4f, 0x89, + 0xe4, 0x28, 0xca, 0x13, 0x4d, 0x96, 0xf3, 0x37, 0xc3, 0x56, 0x6c, 0xf4, 0x40, 0x0d, 0x38, 0xef, + 0x7a, 0x8c, 0x34, 0x07, 0x94, 0x34, 0xf6, 0xdc, 0xfe, 0xbd, 0xad, 0x86, 0xf4, 0x12, 0x07, 0x72, + 0x37, 0xcf, 0xd4, 0x9e, 0xd3, 0xc2, 0xce, 0x6f, 0xa6, 0x75, 0xc2, 0xe9, 0xbc, 0xe8, 0x75, 0x98, + 0x75, 0xbd, 0x66, 0x77, 0xd0, 0x22, 0xdb, 0x0e, 0xef, 0xb0, 0xe5, 0x19, 0x39, 0x8c, 0x85, 0xa3, + 0xc3, 0xb5, 0xd9, 0x4d, 0xa3, 0x1d, 0xc7, 0x7a, 0x09, 0x2e, 0xf2, 0xb1, 0xc1, 0x55, 0x8a, 0xb8, + 0x6e, 0x7c, 0x6c, 0x72, 0x99, 0xbd, 0x52, 0x8a, 0x81, 0x90, 0xa9, 0x18, 0xf8, 0xd3, 0x1c, 0x14, + 0x55, 0x2d, 0x1e, 0x5d, 0x4e, 0x14, 0xbc, 0x9f, 0x1b, 0x2a, 0x78, 0x97, 0xd3, 0xee, 0x2d, 0x6c, + 0x28, 0xba, 0x8c, 0x0d, 0xe2, 0x01, 0xc4, 0xa6, 0x6c, 0xc1, 0x9a, 0x22, 0x4b, 0x5b, 0xbe, 0xb7, + 0xeb, 0xb6, 0x75, 0x01, 0xe2, 0xba, 0x11, 0x36, 0x44, 0xf7, 0xa5, 0x1f, 0x86, 0x17, 0xaa, 0x51, + 0x04, 0x11, 0xeb, 0x20, 0x42, 0x89, 0x3b, 0x8d, 0xbb, 0x6f, 0x2b, 0x19, 0x75, 0x89, 0x88, 0x35, + 0xb2, 0x90, 0xe1, 0x0f, 0x78, 0x7f, 0xc0, 0xe5, 0x46, 0x39, 0x21, 0x19, 0x77, 0x25, 0x22, 0xd6, + 0xc8, 0xf6, 0xa7, 0x16, 0xcc, 0x2b, 0x1d, 0xd4, 0x3b, 0xa4, 0xb9, 0xd7, 0xe0, 0xa4, 0x2f, 0x22, + 0xfa, 0x01, 0x23, 0x2c, 0x19, 0xd1, 0xbf, 0xc3, 0x08, 0xc3, 0x92, 0x62, 0xcc, 0x3e, 0x77, 0x5a, + 0xb3, 0xb7, 0xff, 0xce, 0x82, 0x82, 0x0c, 0x9d, 0xb3, 0xf8, 0x9f, 0x78, 0x39, 0x29, 0x37, 0x56, + 0x39, 0xe9, 0x98, 0x42, 0x5f, 0x54, 0xc9, 0x9a, 0x7a, 0x54, 0x25, 0xcb, 0xfe, 0xda, 0x82, 0xa5, + 0xb4, 0xea, 0x68, 0x96, 0xe1, 0xbf, 0x02, 0x33, 0xfd, 0xae, 0xc3, 0x77, 0x7d, 0xda, 0x4b, 0xde, + 0xb1, 0x6c, 0xeb, 0x76, 0x1c, 0xf6, 0x40, 0x14, 0x80, 0x06, 0x69, 0x58, 0x90, 0xa2, 0x5c, 0xcf, + 0x7a, 0x22, 0xc4, 0xcb, 0x7a, 0x91, 0xb2, 0xc2, 0x26, 0x86, 0x0d, 0x29, 0xf6, 0x9f, 0x14, 0x60, + 0x51, 0xb2, 0x4c, 0x7a, 0x42, 0x4c, 0xb2, 0x42, 0x7d, 0x78, 0x46, 0x26, 0x4f, 0xc3, 0x87, 0x8a, + 0x5a, 0xb4, 0xab, 0x9a, 0xff, 0x99, 0xcd, 0xd4, 0x5e, 0x0f, 0x47, 0x52, 0xf0, 0x08, 0xdc, 0xe1, + 0x93, 0x02, 0xfe, 0xff, 0x9d, 0x14, 0xe6, 0x66, 0x9b, 0x3e, 0x76, 0xb3, 0x8d, 0x3c, 0x57, 0x66, + 0x1e, 0xe3, 0x5c, 0x19, 0xf6, 0xf5, 0xa5, 0x4c, 0xbe, 0xfe, 0x2f, 0x72, 0x30, 0xbd, 0x4d, 0x7d, + 0x59, 0x65, 0x3f, 0xfd, 0x82, 0xed, 0x5d, 0x98, 0x62, 0x7d, 0xd2, 0xd4, 0x3e, 0xef, 0xe2, 0x78, + 0x96, 0xa6, 0x87, 0xd7, 0xe8, 0x93, 0x66, 0x6d, 0x46, 0xb8, 0x51, 0xf1, 0x0b, 0x4b, 0x20, 0xa3, + 0xf2, 0x98, 0xcf, 0x12, 0x81, 0x07, 0x90, 0x8f, 0xae, 0x3c, 0x7e, 0x66, 0x41, 0x59, 0xf7, 0x7c, + 0x6a, 0x4b, 0x5c, 0x7a, 0x7c, 0x23, 0x4a, 0x5c, 0x7f, 0x16, 0xcd, 0x40, 0x28, 0x0d, 0xfd, 0x0e, + 0x2c, 0xf6, 0xa9, 0xdf, 0xf3, 0x85, 0xa5, 0x6e, 0xfb, 0x5d, 0xb7, 0xe9, 0x92, 0xa0, 0x4a, 0x7a, + 0x79, 0x6c, 0x79, 0x06, 0xfb, 0x41, 0xed, 0x1b, 0x5a, 0xee, 0xe2, 0x76, 0x12, 0x17, 0x0f, 0x8b, + 0xb2, 0xff, 0xcd, 0x82, 0xb9, 0x98, 0xee, 0x51, 0x13, 0xa0, 0xe9, 0x7b, 0x2d, 0x97, 0x87, 0xd7, + 0xdc, 0xe5, 0x4b, 0xeb, 0xe3, 0x69, 0xb5, 0x1e, 0xf0, 0x45, 0x9b, 0x2e, 0x6c, 0x62, 0xd8, 0x80, + 0x45, 0xaf, 0x05, 0x2f, 0x4e, 0xe2, 0x41, 0x8c, 0x7a, 0x71, 0xf2, 0xf0, 0x70, 0x6d, 0x56, 0x8f, + 0xc9, 0x7c, 0x81, 0x92, 0xe5, 0xed, 0xc5, 0xdf, 0xe6, 0xa0, 0x14, 0xce, 0xff, 0x09, 0x98, 0xd1, + 0x3b, 0x31, 0x33, 0x7a, 0x2d, 0xe3, 0xca, 0x49, 0x43, 0x0a, 0x63, 0x12, 0xc3, 0x98, 0x3e, 0x48, + 0x18, 0x53, 0xd6, 0x2d, 0x71, 0x8c, 0x39, 0xfd, 0xa3, 0x5a, 0x7c, 0xd5, 0xf7, 0x09, 0x18, 0xd4, + 0xbd, 0xb8, 0x41, 0xad, 0x67, 0x9c, 0xcd, 0x08, 0x93, 0xfa, 0x91, 0x05, 0xf3, 0x09, 0x23, 0x40, + 0xdf, 0x82, 0x82, 0xac, 0x8a, 0xe9, 0xfd, 0x15, 0x32, 0xea, 0x04, 0x5f, 0xd2, 0xd0, 0x36, 0x2c, + 0x39, 0x03, 0xee, 0x87, 0xbc, 0x37, 0x3c, 0x67, 0xa7, 0x4b, 0x54, 0xd6, 0x3e, 0x53, 0xfb, 0x05, + 0xcd, 0xb3, 0x54, 0x4d, 0xe9, 0x83, 0x53, 0x39, 0xed, 0x9f, 0xe5, 0x00, 0x85, 0x8d, 0x59, 0xca, + 0xc9, 0x1f, 0xc0, 0xf4, 0xae, 0x2a, 0x04, 0x3d, 0xde, 0x7d, 0x40, 0xad, 0x6c, 0x5e, 0x89, 0x04, + 0x98, 0xe8, 0x7b, 0x27, 0xb3, 0x8f, 0x60, 0x78, 0x0f, 0xa1, 0x07, 0x00, 0xbb, 0xae, 0xe7, 0xb2, + 0xce, 0x84, 0x37, 0x97, 0xf2, 0x34, 0xbf, 0x19, 0x22, 0x60, 0x03, 0xcd, 0xfe, 0x24, 0x67, 0xec, + 0x4f, 0xe9, 0x2e, 0xc7, 0x5a, 0xd7, 0x97, 0xe2, 0xca, 0x2c, 0x0d, 0xdf, 0x15, 0x19, 0x8a, 0x99, + 0xda, 0x77, 0x68, 0x50, 0xb6, 0xbe, 0x92, 0x51, 0x2d, 0xf7, 0x1d, 0xea, 0x8a, 0x85, 0x8f, 0x96, + 0xf4, 0xbe, 0x43, 0x19, 0x96, 0x90, 0xe8, 0xbb, 0x62, 0xa8, 0xa4, 0x1f, 0xb8, 0xd0, 0xcc, 0x3e, + 0x81, 0x93, 0xbe, 0x39, 0x3f, 0xd2, 0x67, 0x58, 0x01, 0xda, 0x9f, 0x4c, 0x1b, 0x1b, 0x5e, 0x7b, + 0xed, 0x3b, 0x80, 0xba, 0x0e, 0xe3, 0xb7, 0x1d, 0xaf, 0x25, 0xb6, 0x27, 0xd9, 0xa5, 0x84, 0x75, + 0x74, 0x84, 0xb6, 0xa2, 0x51, 0xd0, 0xd6, 0x50, 0x0f, 0x9c, 0xc2, 0x85, 0x2e, 0xc7, 0x9d, 0xf3, + 0x5a, 0xd2, 0x39, 0x9f, 0x8d, 0xac, 0x6d, 0x32, 0xf7, 0x6c, 0x6e, 0xf7, 0xc2, 0x29, 0x6c, 0xf7, + 0xdf, 0x86, 0xc5, 0xdd, 0xe4, 0xdd, 0xa1, 0x7e, 0x49, 0x70, 0x65, 0xc2, 0xab, 0xc7, 0xda, 0xf9, + 0xa3, 0xe8, 0xc2, 0x29, 0x6a, 0xc6, 0xc3, 0x82, 0x90, 0x1f, 0x3c, 0xc8, 0x93, 0xd9, 0xa7, 0x2a, + 0x2c, 0x8c, 0x6d, 0x72, 0x89, 0xbc, 0x35, 0xf9, 0x14, 0x4f, 0x41, 0xe2, 0x98, 0x80, 0x84, 0x09, + 0x16, 0x4f, 0xd2, 0x04, 0xd1, 0xe5, 0xb0, 0xa0, 0x2f, 0x86, 0x23, 0x43, 0xd9, 0xfc, 0x50, 0x29, + 0x5e, 0x90, 0xb0, 0xd9, 0x0f, 0xfd, 0xd8, 0x82, 0xf3, 0x62, 0xb3, 0xde, 0xf8, 0x98, 0x34, 0x07, + 0x42, 0x2b, 0xc1, 0x2b, 0xdc, 0xe5, 0xb2, 0xd4, 0xc6, 0x98, 0xcf, 0x13, 0x1b, 0x69, 0x10, 0x51, + 0x5c, 0x9e, 0x4a, 0xc6, 0xe9, 0x82, 0xd1, 0x87, 0xd2, 0x75, 0x70, 0x22, 0xd3, 0x9e, 0xc7, 0x4f, + 0xef, 0x4b, 0xda, 0xed, 0x70, 0xe5, 0x76, 0x38, 0xb1, 0xff, 0x34, 0xe6, 0xad, 0xc6, 0x2b, 0x3a, + 0xac, 0x40, 0xce, 0x61, 0xc9, 0x32, 0x71, 0x95, 0xe1, 0x9c, 0xc3, 0xd0, 0xf7, 0xa0, 0x40, 0x09, + 0xa7, 0x07, 0xda, 0xa9, 0x5e, 0x9d, 0xc0, 0x81, 0x60, 0xc1, 0xaf, 0x86, 0x2a, 0x7f, 0x62, 0x85, + 0x68, 0xd4, 0x3a, 0xf2, 0xa7, 0x56, 0xeb, 0xf8, 0xa9, 0x65, 0x9c, 0x85, 0xe1, 0x60, 0xd0, 0x3b, + 0x30, 0xcd, 0xdd, 0x1e, 0xf1, 0x07, 0x3c, 0x5b, 0x80, 0x11, 0x5e, 0x4e, 0x49, 0x93, 0xbf, 0xa7, + 0x20, 0x70, 0x80, 0x25, 0xb2, 0x2e, 0x42, 0xa9, 0x4f, 0xef, 0x75, 0x84, 0x0b, 0xf3, 0xbb, 0xea, + 0x14, 0x9f, 0x8b, 0xb2, 0xae, 0x1b, 0x31, 0x2a, 0x4e, 0xf4, 0xb6, 0x29, 0x44, 0xf1, 0x72, 0xf0, + 0xbc, 0x19, 0x7d, 0xa0, 0xa3, 0x3a, 0x2b, 0xcb, 0x93, 0xda, 0x21, 0x98, 0x51, 0xd1, 0x9d, 0xfd, + 0x33, 0x0b, 0xce, 0xa7, 0xf6, 0x0e, 0x8f, 0xa5, 0xdc, 0x29, 0x1e, 0x4b, 0xd6, 0x49, 0x1f, 0x4b, + 0x0f, 0x0c, 0x15, 0x06, 0x43, 0x38, 0xa9, 0x6f, 0x12, 0x7e, 0x92, 0x83, 0x05, 0x4c, 0xfa, 0x7e, + 0xac, 0x42, 0xb3, 0x1d, 0xbc, 0x23, 0xcc, 0x10, 0xd2, 0x24, 0xee, 0x01, 0x6a, 0xd3, 0xb1, 0x07, + 0x84, 0xdf, 0x85, 0x82, 0x2c, 0x9e, 0xe8, 0x20, 0xec, 0x4a, 0x86, 0xbb, 0xf1, 0x18, 0xaa, 0xb4, + 0x38, 0x55, 0x85, 0x52, 0x80, 0x02, 0x59, 0x5e, 0x6b, 0x6b, 0x83, 0xbb, 0x92, 0xe1, 0x82, 0x7c, + 0x18, 0x59, 0x36, 0x63, 0x05, 0x68, 0x7f, 0x9a, 0x03, 0x15, 0xfe, 0x3c, 0x81, 0x34, 0xe7, 0xd7, + 0x63, 0x69, 0xce, 0xfa, 0xb8, 0x4e, 0x5c, 0xa8, 0x67, 0x54, 0x8a, 0x93, 0x0c, 0x4d, 0x2f, 0x66, + 0x01, 0x7d, 0x74, 0x7a, 0xf3, 0x0f, 0x16, 0x94, 0x64, 0xbf, 0x27, 0x90, 0xda, 0x6c, 0xc7, 0x53, + 0x9b, 0x97, 0x33, 0xcc, 0x62, 0x44, 0x5a, 0xf3, 0x49, 0x5e, 0x8f, 0x3e, 0x0c, 0x7c, 0x3b, 0x0e, + 0x6d, 0xe9, 0x90, 0x2e, 0xb2, 0x40, 0xd1, 0x88, 0x15, 0x0d, 0xfd, 0xa6, 0x7a, 0x01, 0x40, 0x18, + 0x27, 0xad, 0x9b, 0x61, 0x7c, 0x95, 0xcf, 0xfc, 0x94, 0x41, 0x3f, 0xb7, 0x88, 0x4a, 0x7b, 0x38, + 0x81, 0x8a, 0x87, 0xe4, 0x88, 0x98, 0xab, 0x9f, 0xf4, 0x65, 0x3a, 0x16, 0xb9, 0x32, 0xa1, 0xe3, + 0x54, 0x31, 0xd7, 0x50, 0x33, 0x1e, 0x16, 0x84, 0x3a, 0x30, 0x6b, 0x3e, 0xc2, 0xd2, 0x7b, 0xe9, + 0x52, 0xf6, 0xd7, 0x5e, 0xea, 0x2a, 0xc7, 0x6c, 0xc1, 0x31, 0x64, 0xfb, 0xb0, 0x08, 0x65, 0x63, + 0xf3, 0x25, 0xca, 0x25, 0x73, 0xa7, 0x53, 0x2e, 0x49, 0x8f, 0xee, 0xcb, 0x13, 0x45, 0xf7, 0x17, + 0xe3, 0xd1, 0xfd, 0x37, 0x93, 0xd1, 0x3d, 0xc8, 0xd9, 0xc5, 0x22, 0x7b, 0x06, 0x67, 0x75, 0x98, + 0x1b, 0xbc, 0xa6, 0xcb, 0x94, 0x2f, 0x0d, 0x07, 0xd3, 0x48, 0x9c, 0xc8, 0x37, 0x63, 0x90, 0x38, + 0x21, 0x42, 0x9c, 0xe8, 0xba, 0xa5, 0x31, 0xe8, 0xf5, 0x1c, 0x7a, 0xb0, 0x3c, 0x2b, 0x07, 0x1c, + 0x9e, 0xe8, 0x37, 0x63, 0x54, 0x9c, 0xe8, 0x8d, 0xb6, 0xa1, 0xa8, 0xa2, 0x64, 0xfd, 0x42, 0xeb, + 0x95, 0x2c, 0x01, 0xb8, 0x8a, 0x68, 0xd4, 0x6f, 0xac, 0x71, 0xcc, 0x04, 0xa7, 0x74, 0x4c, 0x82, + 0x73, 0x07, 0x90, 0xbf, 0x23, 0x63, 0xa7, 0xd6, 0x2d, 0xf5, 0xf1, 0x9e, 0xd8, 0x95, 0x45, 0x19, + 0x3d, 0x87, 0x0b, 0x76, 0x77, 0xa8, 0x07, 0x4e, 0xe1, 0x12, 0x56, 0xad, 0x43, 0xeb, 0xd0, 0x14, + 0x74, 0x32, 0x93, 0x35, 0x24, 0x8c, 0xf2, 0x26, 0xf9, 0xc2, 0xa7, 0x9e, 0x40, 0xc5, 0x43, 0x72, + 0xd0, 0x47, 0x30, 0x27, 0xb6, 0x50, 0x24, 0x18, 0x1e, 0x53, 0xf0, 0xe2, 0xd1, 0xe1, 0xda, 0xdc, + 0x96, 0x09, 0x89, 0xe3, 0x12, 0xec, 0x3f, 0xce, 0x43, 0x7a, 0x60, 0x1f, 0x3d, 0x2e, 0xb6, 0x1e, + 0xf1, 0xb8, 0xf8, 0x5d, 0x28, 0x31, 0xee, 0x50, 0xf5, 0x90, 0x3a, 0x37, 0xd9, 0x43, 0xea, 0x46, + 0x00, 0x80, 0x23, 0xac, 0x44, 0x96, 0x95, 0x3f, 0xd1, 0x2c, 0xeb, 0x12, 0x80, 0x8c, 0x47, 0xeb, + 0xfe, 0x40, 0xdf, 0xab, 0xcc, 0x45, 0x3e, 0xe1, 0x46, 0x48, 0xc1, 0x46, 0x2f, 0x74, 0x35, 0x3c, + 0x38, 0xd5, 0x45, 0xca, 0x85, 0xa1, 0x8b, 0xe0, 0x64, 0x9e, 0x9e, 0xf2, 0x0d, 0xdb, 0x31, 0x0f, + 0x47, 0xec, 0xff, 0xcd, 0x41, 0xcc, 0x19, 0xa2, 0x1f, 0x59, 0xb0, 0xe8, 0x24, 0x3e, 0x03, 0x0c, + 0x62, 0xc9, 0x5f, 0xcb, 0xf6, 0x6d, 0xe6, 0xd0, 0x57, 0x84, 0x51, 0xe9, 0x3a, 0xd9, 0x85, 0xe1, + 0x61, 0xa1, 0xe8, 0x0f, 0x2d, 0x38, 0xe7, 0x0c, 0x7f, 0xe7, 0xa9, 0x17, 0xfd, 0xcd, 0x89, 0x3f, + 0x14, 0xad, 0x3d, 0x7b, 0x74, 0xb8, 0x96, 0xf6, 0x05, 0x2c, 0x4e, 0x13, 0x87, 0xde, 0x83, 0x29, + 0x87, 0xb6, 0x83, 0x32, 0x4f, 0x76, 0xb1, 0xc1, 0xe7, 0xbb, 0x51, 0x74, 0x54, 0xa5, 0x6d, 0x86, + 0x25, 0xa8, 0xfd, 0xf3, 0x3c, 0x2c, 0x24, 0x1f, 0x23, 0xeb, 0xb7, 0x45, 0x53, 0xa9, 0x6f, 0x8b, + 0x84, 0x8d, 0x34, 0x79, 0xf8, 0xd0, 0x27, 0xb2, 0x11, 0xd1, 0x88, 0x15, 0x2d, 0xb4, 0x11, 0xf9, + 0x44, 0xb0, 0xf0, 0x18, 0x36, 0x22, 0xdf, 0x05, 0x46, 0x58, 0xe8, 0x6a, 0xfc, 0x6c, 0xb1, 0x93, + 0x67, 0xcb, 0xa2, 0x39, 0x97, 0x49, 0x8b, 0x47, 0x3d, 0x28, 0x1b, 0xeb, 0xa0, 0x2d, 0xf1, 0x5a, + 0x66, 0xbd, 0x47, 0xdb, 0x6e, 0x5e, 0x7d, 0x03, 0x1c, 0x51, 0x4c, 0xfc, 0xc8, 0xee, 0xa5, 0xb6, + 0x1e, 0xab, 0xba, 0x22, 0xd5, 0x65, 0xa0, 0xd9, 0xff, 0x6e, 0xc1, 0x5c, 0xec, 0xc1, 0x9b, 0x90, + 0x16, 0x3c, 0x2c, 0x9c, 0xfc, 0xab, 0xd8, 0xfb, 0x21, 0x02, 0x36, 0xd0, 0xd0, 0xf7, 0xa1, 0xdc, + 0xf5, 0xbd, 0x36, 0x61, 0xbc, 0xe1, 0x3b, 0x7b, 0xda, 0x4e, 0xb2, 0xa6, 0xdf, 0xcb, 0x47, 0x87, + 0x6b, 0x4b, 0x5b, 0x0a, 0xa6, 0xee, 0xf7, 0xfa, 0x5d, 0xc2, 0xd5, 0x8b, 0x50, 0x6c, 0x82, 0xcb, + 0x0b, 0x98, 0x77, 0x1d, 0x4a, 0x3a, 0xfe, 0x80, 0x91, 0xa7, 0xf5, 0x02, 0x26, 0x1c, 0xe0, 0x49, + 0x5f, 0xc0, 0x44, 0xc0, 0xc7, 0x5f, 0xc0, 0x84, 0x7d, 0x9f, 0xda, 0x0b, 0x98, 0x70, 0x84, 0x23, + 0x32, 0x95, 0xff, 0xc9, 0x19, 0xb3, 0x88, 0x67, 0x2b, 0xb9, 0x47, 0x64, 0x2b, 0xef, 0xc3, 0x8c, + 0xeb, 0x71, 0x42, 0xf7, 0x9d, 0xae, 0x2e, 0x71, 0x65, 0xdd, 0x8b, 0xe1, 0x54, 0x37, 0x35, 0x0e, + 0x0e, 0x11, 0x51, 0x17, 0xce, 0x07, 0xa5, 0x59, 0x4a, 0x9c, 0xe8, 0x6a, 0x48, 0x3f, 0xaf, 0x78, + 0x23, 0xa8, 0x21, 0xde, 0x4c, 0xeb, 0xf4, 0x70, 0x14, 0x01, 0xa7, 0x83, 0x22, 0x06, 0x73, 0xcc, + 0x48, 0xd3, 0x83, 0x13, 0x71, 0xcc, 0xb2, 0x76, 0xb2, 0xb2, 0x61, 0xbc, 0xc9, 0x30, 0x41, 0x71, + 0x5c, 0x86, 0xfd, 0x2f, 0x79, 0x98, 0x4f, 0xec, 0xb4, 0x44, 0x3a, 0x52, 0x7a, 0x92, 0xe9, 0x48, + 0x71, 0xa2, 0x74, 0x24, 0x3d, 0x52, 0x9e, 0x9a, 0x28, 0x52, 0x7e, 0x4b, 0x45, 0xab, 0x7a, 0xe5, + 0x36, 0x37, 0xf4, 0x13, 0xd6, 0x50, 0x9b, 0x5b, 0x26, 0x11, 0xc7, 0xfb, 0xca, 0x70, 0xa2, 0x35, + 0xfc, 0x4d, 0xac, 0x0e, 0xb5, 0xdf, 0xcc, 0xfa, 0x06, 0x29, 0x04, 0x50, 0xe1, 0x44, 0x0a, 0x01, + 0xa7, 0x89, 0xab, 0xdd, 0xf9, 0xfc, 0xab, 0xd5, 0x33, 0x5f, 0x7c, 0xb5, 0x7a, 0xe6, 0xcb, 0xaf, + 0x56, 0xcf, 0xfc, 0xf0, 0x68, 0xd5, 0xfa, 0xfc, 0x68, 0xd5, 0xfa, 0xe2, 0x68, 0xd5, 0xfa, 0xf2, + 0x68, 0xd5, 0xfa, 0xcf, 0xa3, 0x55, 0xeb, 0xc7, 0x5f, 0xaf, 0x9e, 0x79, 0xf0, 0xfc, 0x38, 0xff, + 0x75, 0xe5, 0xff, 0x02, 0x00, 0x00, 0xff, 0xff, 0xd4, 0xe7, 0x00, 0x2a, 0x9c, 0x45, 0x00, 0x00, } func (m *AnalysisRunArgument) Marshal() (dAtA []byte, err error) { @@ -2425,6 +2463,41 @@ func (m *ChartSubscription) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *CurrentStage) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CurrentStage) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *CurrentStage) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Since != nil { + { + size, err := m.Since.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func (m *DiscoveredArtifacts) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -3009,6 +3082,18 @@ func (m *FreightSources) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.RequiredSoakTime != nil { + { + size, err := m.RequiredSoakTime.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } if len(m.Stages) > 0 { for iNdEx := len(m.Stages) - 1; iNdEx >= 0; iNdEx-- { i -= len(m.Stages[iNdEx]) @@ -3049,6 +3134,35 @@ func (m *FreightStatus) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.CurrentlyIn) > 0 { + keysForCurrentlyIn := make([]string, 0, len(m.CurrentlyIn)) + for k := range m.CurrentlyIn { + keysForCurrentlyIn = append(keysForCurrentlyIn, string(k)) + } + github_com_gogo_protobuf_sortkeys.Strings(keysForCurrentlyIn) + for iNdEx := len(keysForCurrentlyIn) - 1; iNdEx >= 0; iNdEx-- { + v := m.CurrentlyIn[string(keysForCurrentlyIn[iNdEx])] + baseI := i + { + size, err := (&v).MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + i -= len(keysForCurrentlyIn[iNdEx]) + copy(dAtA[i:], keysForCurrentlyIn[iNdEx]) + i = encodeVarintGenerated(dAtA, i, uint64(len(keysForCurrentlyIn[iNdEx]))) + i-- + dAtA[i] = 0xa + i = encodeVarintGenerated(dAtA, i, uint64(baseI-i)) + i-- + dAtA[i] = 0x1a + } + } if len(m.ApprovedFor) > 0 { keysForApprovedFor := make([]string, 0, len(m.ApprovedFor)) for k := range m.ApprovedFor { @@ -4914,6 +5028,18 @@ func (m *VerifiedStage) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.LongestCompletedSoak != nil { + { + size, err := m.LongestCompletedSoak.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } if m.VerifiedAt != nil { { size, err := m.VerifiedAt.MarshalToSizedBuffer(dAtA[:i]) @@ -5337,6 +5463,19 @@ func (m *ChartSubscription) Size() (n int) { return n } +func (m *CurrentStage) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Since != nil { + l = m.Since.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + return n +} + func (m *DiscoveredArtifacts) Size() (n int) { if m == nil { return 0 @@ -5558,6 +5697,10 @@ func (m *FreightSources) Size() (n int) { n += 1 + l + sovGenerated(uint64(l)) } } + if m.RequiredSoakTime != nil { + l = m.RequiredSoakTime.Size() + n += 1 + l + sovGenerated(uint64(l)) + } return n } @@ -5585,6 +5728,15 @@ func (m *FreightStatus) Size() (n int) { n += mapEntrySize + 1 + sovGenerated(uint64(mapEntrySize)) } } + if len(m.CurrentlyIn) > 0 { + for k, v := range m.CurrentlyIn { + _ = k + _ = v + l = v.Size() + mapEntrySize := 1 + len(k) + sovGenerated(uint64(len(k))) + 1 + l + sovGenerated(uint64(l)) + n += mapEntrySize + 1 + sovGenerated(uint64(mapEntrySize)) + } + } return n } @@ -6266,6 +6418,10 @@ func (m *VerifiedStage) Size() (n int) { l = m.VerifiedAt.Size() n += 1 + l + sovGenerated(uint64(l)) } + if m.LongestCompletedSoak != nil { + l = m.LongestCompletedSoak.Size() + n += 1 + l + sovGenerated(uint64(l)) + } return n } @@ -6500,6 +6656,16 @@ func (this *ChartSubscription) String() string { }, "") return s } +func (this *CurrentStage) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&CurrentStage{`, + `Since:` + strings.Replace(fmt.Sprintf("%v", this.Since), "Time", "v1.Time", 1) + `,`, + `}`, + }, "") + return s +} func (this *DiscoveredArtifacts) String() string { if this == nil { return "nil" @@ -6678,6 +6844,7 @@ func (this *FreightSources) String() string { s := strings.Join([]string{`&FreightSources{`, `Direct:` + fmt.Sprintf("%v", this.Direct) + `,`, `Stages:` + fmt.Sprintf("%v", this.Stages) + `,`, + `RequiredSoakTime:` + strings.Replace(fmt.Sprintf("%v", this.RequiredSoakTime), "Duration", "v1.Duration", 1) + `,`, `}`, }, "") return s @@ -6706,9 +6873,20 @@ func (this *FreightStatus) String() string { mapStringForApprovedFor += fmt.Sprintf("%v: %v,", k, this.ApprovedFor[k]) } mapStringForApprovedFor += "}" + keysForCurrentlyIn := make([]string, 0, len(this.CurrentlyIn)) + for k := range this.CurrentlyIn { + keysForCurrentlyIn = append(keysForCurrentlyIn, k) + } + github_com_gogo_protobuf_sortkeys.Strings(keysForCurrentlyIn) + mapStringForCurrentlyIn := "map[string]CurrentStage{" + for _, k := range keysForCurrentlyIn { + mapStringForCurrentlyIn += fmt.Sprintf("%v: %v,", k, this.CurrentlyIn[k]) + } + mapStringForCurrentlyIn += "}" s := strings.Join([]string{`&FreightStatus{`, `VerifiedIn:` + mapStringForVerifiedIn + `,`, `ApprovedFor:` + mapStringForApprovedFor + `,`, + `CurrentlyIn:` + mapStringForCurrentlyIn + `,`, `}`, }, "") return s @@ -7214,6 +7392,7 @@ func (this *VerifiedStage) String() string { } s := strings.Join([]string{`&VerifiedStage{`, `VerifiedAt:` + strings.Replace(fmt.Sprintf("%v", this.VerifiedAt), "Time", "v1.Time", 1) + `,`, + `LongestCompletedSoak:` + strings.Replace(fmt.Sprintf("%v", this.LongestCompletedSoak), "Duration", "v1.Duration", 1) + `,`, `}`, }, "") return s @@ -8952,6 +9131,92 @@ func (m *ChartSubscription) Unmarshal(dAtA []byte) error { } return nil } +func (m *CurrentStage) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: CurrentStage: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CurrentStage: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Since", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Since == nil { + m.Since = &v1.Time{} + } + if err := m.Since.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *DiscoveredArtifacts) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -10770,6 +11035,42 @@ func (m *FreightSources) Unmarshal(dAtA []byte) error { } m.Stages = append(m.Stages, string(dAtA[iNdEx:postIndex])) iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field RequiredSoakTime", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.RequiredSoakTime == nil { + m.RequiredSoakTime = &v1.Duration{} + } + if err := m.RequiredSoakTime.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -11078,6 +11379,135 @@ func (m *FreightStatus) Unmarshal(dAtA []byte) error { } m.ApprovedFor[mapkey] = *mapvalue iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field CurrentlyIn", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.CurrentlyIn == nil { + m.CurrentlyIn = make(map[string]CurrentStage) + } + var mapkey string + mapvalue := &CurrentStage{} + for iNdEx < postIndex { + entryPreIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + if fieldNum == 1 { + var stringLenmapkey uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLenmapkey |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLenmapkey := int(stringLenmapkey) + if intStringLenmapkey < 0 { + return ErrInvalidLengthGenerated + } + postStringIndexmapkey := iNdEx + intStringLenmapkey + if postStringIndexmapkey < 0 { + return ErrInvalidLengthGenerated + } + if postStringIndexmapkey > l { + return io.ErrUnexpectedEOF + } + mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) + iNdEx = postStringIndexmapkey + } else if fieldNum == 2 { + var mapmsglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + mapmsglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if mapmsglen < 0 { + return ErrInvalidLengthGenerated + } + postmsgIndex := iNdEx + mapmsglen + if postmsgIndex < 0 { + return ErrInvalidLengthGenerated + } + if postmsgIndex > l { + return io.ErrUnexpectedEOF + } + mapvalue = &CurrentStage{} + if err := mapvalue.Unmarshal(dAtA[iNdEx:postmsgIndex]); err != nil { + return err + } + iNdEx = postmsgIndex + } else { + iNdEx = entryPreIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > postIndex { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + m.CurrentlyIn[mapkey] = *mapvalue + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -16768,6 +17198,42 @@ func (m *VerifiedStage) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LongestCompletedSoak", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.LongestCompletedSoak == nil { + m.LongestCompletedSoak = &v1.Duration{} + } + if err := m.LongestCompletedSoak.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) diff --git a/api/v1alpha1/generated.proto b/api/v1alpha1/generated.proto index 68f952b83..4b94f2824 100644 --- a/api/v1alpha1/generated.proto +++ b/api/v1alpha1/generated.proto @@ -181,6 +181,14 @@ message ChartSubscription { optional int32 discoveryLimit = 4; } +// CurrentStage reflects a Stage's current use of Freight. +message CurrentStage { + // Since is the time at which the Stage most recently started using the + // Freight. This can be used to calculate how long the Freight has been in use + // by the Stage. + optional .k8s.io.apimachinery.pkg.apis.meta.v1.Time since = 1; +} + // DiscoveredArtifacts holds the artifacts discovered by the Warehouse for its // subscriptions. message DiscoveredArtifacts { @@ -335,7 +343,7 @@ message FreightOrigin { optional string kind = 1; // Name is the name of the resource of the kind indicated by the Kind field - // from which Freight may originated. + // from which Freight may originate. // // +kubebuilder:validation:Required optional string name = 2; @@ -388,10 +396,26 @@ message FreightSources { // Direct field must be true. i.e. Between the two fields, at least on source // must be specified. repeated string stages = 2; + + // RequiredSoakTime specifies a minimum duration for which the requested + // Freight must have continuously occupied ("soaked in") in an upstream Stage + // before becoming available for promotion to this Stage. This is an optional + // field. If nil or zero, no soak time is required. Any soak time requirement + // is in ADDITION to the requirement that Freight be verified in an upstream + // Stage to become available for promotion to this Stage, although a manual + // approval for promotion to this Stage will supersede any soak time + // requirement. + // + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(s|m|h))+$" + optional .k8s.io.apimachinery.pkg.apis.meta.v1.Duration requiredSoakTime = 3; } // FreightStatus describes a piece of Freight's most recently observed state. message FreightStatus { + // CurrentlyIn describes the Stages in which this Freight is currently in use. + map currentlyIn = 3; + // VerifiedIn describes the Stages in which this Freight has been verified // through promotion and subsequent health checks. map verifiedIn = 1; @@ -1201,6 +1225,13 @@ message VerificationInfo { message VerifiedStage { // VerifiedAt is the time at which the Freight was verified in the Stage. optional .k8s.io.apimachinery.pkg.apis.meta.v1.Time verifiedAt = 1; + + // LongestCompletedSoak represents the longest definite time interval wherein + // the Freight was in CONTINUOUS use by the Stage. This value is updated as + // Freight EXITS the Stage. If the Freight is currently in use by the Stage, + // the time elapsed since the Freight ENTERED the Stage is its current soak + // time, which may exceed the value of this field. + optional .k8s.io.apimachinery.pkg.apis.meta.v1.Duration longestSoak = 2; } // Warehouse is a source of Freight. diff --git a/api/v1alpha1/stage_helpers.go b/api/v1alpha1/stage_helpers.go index f5873c3c7..d62a36330 100644 --- a/api/v1alpha1/stage_helpers.go +++ b/api/v1alpha1/stage_helpers.go @@ -5,6 +5,8 @@ import ( "encoding/json" "errors" "fmt" + "slices" + "strings" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -97,6 +99,92 @@ func GetStage( return &stage, nil } +// ListAvailableFreight lists all Freight available to the Stage for any reason. +// This includes: +// +// 1. Any Freight from a Warehouse that the Stage subscribes to directly +// 2. Any Freight that is verified in any upstream Stages (with any applicable soak time elapsed) +// 3. Any Freight that is approved for the Stage +func (s *Stage) ListAvailableFreight( + ctx context.Context, + c client.Client, +) ([]Freight, error) { + availableFreight := []Freight{} + + for _, req := range s.Spec.RequestedFreight { + // Get the Warehouse of origin + warehouse, err := GetWarehouse( + ctx, + c, + types.NamespacedName{ + Namespace: s.Namespace, + Name: req.Origin.Name, + }, + ) + if err != nil { + return nil, err + } + if warehouse == nil { + return nil, fmt.Errorf( + "Warehouse %q not found in namespace %q", + req.Origin.Name, + s.Namespace, + ) + } + // Get applicable Freight from the Warehouse + var listOpts *ListWarehouseFreightOptions + if !req.Sources.Direct { + listOpts = &ListWarehouseFreightOptions{ + ApprovedFor: s.Name, + VerifiedIn: req.Sources.Stages, + } + if requiredSoak := req.Sources.RequiredSoakTime; requiredSoak != nil { + listOpts.VerifiedBefore = &metav1.Time{Time: time.Now().Add(-requiredSoak.Duration)} + } + } + freightFromWarehouse, err := warehouse.ListFreight(ctx, c, listOpts) + if err != nil { + return nil, err + } + availableFreight = append(availableFreight, freightFromWarehouse...) + } + + // Sort and de-dupe the available Freight + slices.SortFunc(availableFreight, func(lhs, rhs Freight) int { + return strings.Compare(lhs.Name, rhs.Name) + }) + availableFreight = slices.CompactFunc(availableFreight, func(lhs, rhs Freight) bool { + return lhs.Name == rhs.Name + }) + + return availableFreight, nil +} + +// IsFreightAvailable answers whether the specified Freight is available to the +// Stage. +func (s *Stage) IsFreightAvailable(freight *Freight) bool { + if s == nil || freight == nil || s.Namespace != freight.Namespace { + return false + } + if freight.IsApprovedFor(s.Name) { + return true + } + for _, req := range s.Spec.RequestedFreight { + if freight.Origin.Equals(&req.Origin) { + if req.Sources.Direct { + return true + } + for _, source := range req.Sources.Stages { + if freight.IsVerifiedIn(source) { + return req.Sources.RequiredSoakTime == nil || + freight.GetLongestSoak(source) >= req.Sources.RequiredSoakTime.Duration + } + } + } + } + return false +} + // RefreshStage forces reconciliation of a Stage by setting an annotation // on the Stage, causing the controller to reconcile it. Currently, the // annotation value is the timestamp of the request, but might in the diff --git a/api/v1alpha1/stage_helpers_test.go b/api/v1alpha1/stage_helpers_test.go index 87bc52251..6969a544b 100644 --- a/api/v1alpha1/stage_helpers_test.go +++ b/api/v1alpha1/stage_helpers_test.go @@ -2,7 +2,9 @@ package v1alpha1 import ( "context" + "errors" "testing" + "time" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -10,6 +12,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" ) func TestVerificationRequest_Equals(t *testing.T) { @@ -193,6 +196,420 @@ func TestGetStage(t *testing.T) { } } +func TestStage_ListAvailableFreight(t *testing.T) { + const testProject = "fake-namespace" + const testWarehouse1 = "fake-warehouse1" + const testWarehouse2 = "fake-warehouse2" + const testStage = "fake-stage" + + testWarehouse1Origin := FreightOrigin{ + Kind: FreightOriginKindWarehouse, + Name: testWarehouse1, + } + + testWarehouse2Origin := FreightOrigin{ + Kind: FreightOriginKindWarehouse, + Name: testWarehouse2, + } + + testCases := []struct { + name string + reqs []FreightRequest + objects []client.Object + interceptor interceptor.Funcs + assertions func(*testing.T, []Freight, error) + }{ + { + name: "error getting Warehouse", + reqs: []FreightRequest{{}}, + interceptor: interceptor.Funcs{ + Get: func( + context.Context, + client.WithWatch, + client.ObjectKey, + client.Object, + ...client.GetOption, + ) error { + return errors.New("something went wrong") + }, + }, + assertions: func(t *testing.T, _ []Freight, err error) { + require.ErrorContains(t, err, "error getting Warehouse") + require.ErrorContains(t, err, "something went wrong") + }, + }, + { + name: "Warehouse not found", + reqs: []FreightRequest{{}}, + assertions: func(t *testing.T, _ []Freight, err error) { + require.ErrorContains(t, err, "Warehouse") + require.ErrorContains(t, err, "not found") + }, + }, + { + name: "error listing Freight", + reqs: []FreightRequest{{Origin: testWarehouse1Origin}}, + objects: []client.Object{ + &Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: testWarehouse1, + }, + }, + }, + interceptor: interceptor.Funcs{ + List: func( + context.Context, + client.WithWatch, + client.ObjectList, + ...client.ListOption, + ) error { + return errors.New("something went wrong") + }, + }, + assertions: func(t *testing.T, _ []Freight, err error) { + require.ErrorContains(t, err, "error listing Freight for Warehouse") + require.ErrorContains(t, err, "something went wrong") + }, + }, + { + name: "success", + reqs: []FreightRequest{ + { + Origin: testWarehouse1Origin, + Sources: FreightSources{Direct: true}, + }, + { + Origin: testWarehouse2Origin, + Sources: FreightSources{ + Stages: []string{testStage}, + RequiredSoakTime: &metav1.Duration{Duration: time.Hour}, + }, + }, + }, + objects: []client.Object{ + &Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: testWarehouse1, + }, + }, + &Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: testWarehouse2, + }, + }, + &Freight{ // Available because Freight is requested directly from this Warehouse + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight-1", + }, + Origin: testWarehouse1Origin, + }, + &Freight{ // Not available because Freight is not verified in upstream Stage + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight-2", + }, + Origin: testWarehouse2Origin, + }, + &Freight{ // Not available because verification time not recorded + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight-3", + }, + Origin: testWarehouse2Origin, + Status: FreightStatus{ + VerifiedIn: map[string]VerifiedStage{ + testStage: {}, + }, + }, + }, + &Freight{ // Not available because soak time has not elapsed + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight-4", + }, + Origin: testWarehouse2Origin, + Status: FreightStatus{ + VerifiedIn: map[string]VerifiedStage{ + testStage: { + VerifiedAt: &metav1.Time{Time: time.Now()}, + }, + }, + }, + }, + &Freight{ // Available because soak time has elapsed + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight-5", + }, + Origin: testWarehouse2Origin, + Status: FreightStatus{ + VerifiedIn: map[string]VerifiedStage{ + testStage: { + VerifiedAt: &metav1.Time{Time: time.Now().Add(-time.Hour * 2)}, + }, + }, + }, + }, + }, + assertions: func(t *testing.T, freight []Freight, err error) { + require.NoError(t, err) + require.Len(t, freight, 2) + require.Equal(t, "fake-freight-1", freight[0].Name) + require.Equal(t, "fake-freight-5", freight[1].Name) + }, + }, + } + + testScheme := k8sruntime.NewScheme() + err := SchemeBuilder.AddToScheme(testScheme) + require.NoError(t, err) + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + c := fake.NewClientBuilder().WithScheme(testScheme). + WithScheme(testScheme). + WithIndex(&Freight{}, warehouseField, warehouseIndexer). + WithIndex(&Freight{}, approvedField, approvedForIndexer). + WithIndex(&Freight{}, verifiedInField, verifiedInIndexer). + WithObjects(testCase.objects...). + WithInterceptorFuncs(testCase.interceptor). + Build() + + stage := &Stage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: testStage, + }, + Spec: StageSpec{ + RequestedFreight: testCase.reqs, + }, + } + freight, err := stage.ListAvailableFreight(context.Background(), c) + testCase.assertions(t, freight, err) + }) + } +} + +func TestStage_IsFreightAvailable(t *testing.T) { + const testNamespace = "fake-namespace" + const testWarehouse = "fake-warehouse" + const testStage = "fake-stage" + const testFreight = "fake-freight" + testStageMeta := metav1.ObjectMeta{ + Namespace: testNamespace, + Name: testStage, + } + testFreightMeta := metav1.ObjectMeta{ + Namespace: testNamespace, + Name: testFreight, + } + testOrigin := FreightOrigin{ + Kind: FreightOriginKindWarehouse, + Name: testWarehouse, + } + + testCases := []struct { + name string + stage *Stage + freight *Freight + expected bool + }{ + { + name: "stage is nil", + freight: &Freight{ObjectMeta: testFreightMeta}, + expected: false, + }, + { + name: "freight is nil", + stage: &Stage{ObjectMeta: testStageMeta}, + expected: false, + }, + { + name: "stage and freight are in different namespaces", + stage: &Stage{ObjectMeta: testStageMeta}, + freight: &Freight{ + ObjectMeta: metav1.ObjectMeta{Namespace: "wrong-namespace"}, + }, + expected: false, + }, + { + name: "freight is approved for stage", + stage: &Stage{ObjectMeta: testStageMeta}, + freight: &Freight{ + ObjectMeta: testFreightMeta, + Status: FreightStatus{ + ApprovedFor: map[string]ApprovedStage{ + testStage: {}, + }, + }, + }, + expected: true, + }, + { + name: "stage accepts freight direct from origin", + stage: &Stage{ + ObjectMeta: testStageMeta, + Spec: StageSpec{ + RequestedFreight: []FreightRequest{{ + Origin: testOrigin, + Sources: FreightSources{ + Direct: true, + }, + }}, + }, + }, + freight: &Freight{ + ObjectMeta: testFreightMeta, + Origin: testOrigin, + }, + expected: true, + }, + { + name: "freight is verified in an upstream; soak not required", + stage: &Stage{ + ObjectMeta: testStageMeta, + Spec: StageSpec{ + RequestedFreight: []FreightRequest{{ + Origin: testOrigin, + Sources: FreightSources{ + Stages: []string{"upstream-stage"}, + }, + }}, + }, + }, + freight: &Freight{ + ObjectMeta: testFreightMeta, + Origin: testOrigin, + Status: FreightStatus{ + VerifiedIn: map[string]VerifiedStage{ + "upstream-stage": {}, + }, + }, + }, + expected: true, + }, + { + name: "freight is verified in an upstream stage with no longestCompletedSoak; soak required", + stage: &Stage{ + ObjectMeta: testStageMeta, + Spec: StageSpec{ + RequestedFreight: []FreightRequest{{ + Origin: testOrigin, + Sources: FreightSources{ + Stages: []string{"upstream-stage"}, + RequiredSoakTime: &metav1.Duration{Duration: time.Hour}, + }, + }}, + }, + }, + freight: &Freight{ + ObjectMeta: testFreightMeta, + Origin: testOrigin, + Status: FreightStatus{ + VerifiedIn: map[string]VerifiedStage{ + "upstream-stage": {}, + }, + }, + }, + expected: false, + }, + { + name: "freight is verified in an upstream stage with longestCompletedSoak; soak required but not elapsed", + stage: &Stage{ + ObjectMeta: testStageMeta, + Spec: StageSpec{ + RequestedFreight: []FreightRequest{{ + Origin: testOrigin, + Sources: FreightSources{ + Stages: []string{"upstream-stage"}, + RequiredSoakTime: &metav1.Duration{Duration: 2 * time.Hour}, + }, + }}, + }, + }, + freight: &Freight{ + ObjectMeta: testFreightMeta, + Origin: testOrigin, + Status: FreightStatus{ + VerifiedIn: map[string]VerifiedStage{ + "upstream-stage": { + LongestCompletedSoak: &metav1.Duration{Duration: time.Hour}, + }, + }, + }, + }, + expected: false, + }, + { + name: "freight is verified in an upstream stage with longestCompletedSoak; soak required and is elapsed", + stage: &Stage{ + ObjectMeta: testStageMeta, + Spec: StageSpec{ + RequestedFreight: []FreightRequest{{ + Origin: testOrigin, + Sources: FreightSources{ + Stages: []string{"upstream-stage"}, + RequiredSoakTime: &metav1.Duration{Duration: time.Hour}, + }, + }}, + }, + }, + freight: &Freight{ + ObjectMeta: testFreightMeta, + Origin: testOrigin, + Status: FreightStatus{ + VerifiedIn: map[string]VerifiedStage{ + "upstream-stage": { + LongestCompletedSoak: &metav1.Duration{Duration: time.Hour}, + }, + }, + }, + }, + expected: true, + }, + { + name: "freight from origin not requested", + stage: &Stage{ + ObjectMeta: testStageMeta, + Spec: StageSpec{ + RequestedFreight: []FreightRequest{{ + Origin: testOrigin, + Sources: FreightSources{ + Stages: []string{"upstream-stage"}, + }, + }}, + }, + }, + freight: &Freight{ + ObjectMeta: testFreightMeta, + Origin: FreightOrigin{ + Kind: FreightOriginKindWarehouse, + Name: "wrong-warehouse", + }, + Status: FreightStatus{ + VerifiedIn: map[string]VerifiedStage{ + "upstream-stage": {}, + }, + }, + }, + expected: false, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + require.Equal( + t, + testCase.expected, + testCase.stage.IsFreightAvailable(testCase.freight), + ) + }) + } +} + func TestReverifyStageFreight(t *testing.T) { scheme := k8sruntime.NewScheme() require.NoError(t, SchemeBuilder.AddToScheme(scheme)) diff --git a/api/v1alpha1/stage_types.go b/api/v1alpha1/stage_types.go index 3d61284d6..c8ba86ce7 100644 --- a/api/v1alpha1/stage_types.go +++ b/api/v1alpha1/stage_types.go @@ -218,7 +218,7 @@ type FreightOrigin struct { // +kubebuilder:validation:Required Kind FreightOriginKind `json:"kind" protobuf:"bytes,1,opt,name=kind"` // Name is the name of the resource of the kind indicated by the Kind field - // from which Freight may originated. + // from which Freight may originate. // // +kubebuilder:validation:Required Name string `json:"name" protobuf:"bytes,2,opt,name=name"` @@ -252,6 +252,18 @@ type FreightSources struct { // Direct field must be true. i.e. Between the two fields, at least on source // must be specified. Stages []string `json:"stages,omitempty" protobuf:"bytes,2,rep,name=stages"` + // RequiredSoakTime specifies a minimum duration for which the requested + // Freight must have continuously occupied ("soaked in") in an upstream Stage + // before becoming available for promotion to this Stage. This is an optional + // field. If nil or zero, no soak time is required. Any soak time requirement + // is in ADDITION to the requirement that Freight be verified in an upstream + // Stage to become available for promotion to this Stage, although a manual + // approval for promotion to this Stage will supersede any soak time + // requirement. + // + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(s|m|h))+$" + RequiredSoakTime *metav1.Duration `json:"requiredSoakTime,omitempty" protobuf:"bytes,3,opt,name=requiredSoakTime"` } // PromotionTemplate defines a template for a Promotion that can be used to @@ -362,6 +374,20 @@ type FreightCollection struct { VerificationHistory VerificationInfoStack `json:"verificationHistory,omitempty" protobuf:"bytes,2,rep,name=verificationHistory"` } +// Includes returns true if the FreightCollection includes Freight with the +// specified name and false otherwise. +func (f *FreightCollection) Includes(freightName string) bool { + if f == nil { + return false + } + for _, freight := range f.Freight { + if freight.Name == freightName { + return true + } + } + return false +} + // UpdateOrPush updates the entry in the FreightCollection based on the // Warehouse name of the provided FreightReference. If no such entry exists, the // provided FreightReference is appended to the FreightCollection. This function diff --git a/api/v1alpha1/stage_types_test.go b/api/v1alpha1/stage_types_test.go index 4bb41575d..a5d57072d 100644 --- a/api/v1alpha1/stage_types_test.go +++ b/api/v1alpha1/stage_types_test.go @@ -37,6 +37,50 @@ func TestVerificationInfo_HasAnalysisRun(t *testing.T) { } } +func TestFreightCollectionIncludes(t *testing.T) { + const testFreight = "test-freight" + testCases := []struct { + name string + collection *FreightCollection + expected bool + }{ + { + name: "collection is nil", + collection: nil, + expected: false, + }, + { + name: "collection.Freight is nil", + collection: &FreightCollection{}, + expected: false, + }, + { + name: "collection does not include Freight", + collection: &FreightCollection{ + Freight: map[string]FreightReference{ + "fake-warehouse": {Name: "wrong-freight"}, + "another-fake-warehouse": {Name: "another-wrong-freight"}, + }, + }, + expected: false, + }, + { + name: "collection includes Freight", + collection: &FreightCollection{ + Freight: map[string]FreightReference{ + "fake-warehouse": {Name: testFreight}, + }, + }, + expected: true, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + require.Equal(t, testCase.expected, testCase.collection.Includes(testFreight)) + }) + } +} + func TestFreightCollectionUpdateOrPush(t *testing.T) { fooOrigin := FreightOrigin{ Kind: FreightOriginKindWarehouse, diff --git a/api/v1alpha1/warehouse_helpers.go b/api/v1alpha1/warehouse_helpers.go index 71e1d5494..0435932bf 100644 --- a/api/v1alpha1/warehouse_helpers.go +++ b/api/v1alpha1/warehouse_helpers.go @@ -3,9 +3,12 @@ package v1alpha1 import ( "context" "fmt" + "slices" + "strings" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -59,3 +62,124 @@ func RefreshWarehouse( } return warehouse, nil } + +// ListWarehouseFreightOptions is a struct that can be used to specify filtering +// criteria when listing Freight resources that originated from a Warehouse. +// +// +protobuf=false +// +k8s:deepcopy-gen=false +// +k8s:openapi-gen=false +type ListWarehouseFreightOptions struct { + // ApprovedFor names a Stage for which all Freight resources that have been + // approved for that Stage should be included in the list results. + // + // IMPORTANT: This is OR'ed with the VerifiedIn field. + ApprovedFor string + // VerifiedIn names zero or more Stages for which all Freight resources that + // have been verified for those Stages should be included in the list results + // AS long as they were verified before the VerifiedBefore time (if set). + // + // IMPORTANT: This is OR'ed with the ApprovedFor field. + VerifiedIn []string + // VerifiedBefore optionally specifies a time before which a Freight verified + // in any of the Stages named in the VerifiedIn field must have been verified. + // This is useful for filtering out Freight whose soak time has not yet + // elapsed. + VerifiedBefore *metav1.Time +} + +// ListFreight returns a list of all Freight resources that originated from the +// Warehouse. +func (w *Warehouse) ListFreight( + ctx context.Context, + c client.Client, + opts *ListWarehouseFreightOptions, +) ([]Freight, error) { + if opts == nil { + opts = &ListWarehouseFreightOptions{} + } + + // Build a list of list options to make multiple queries whose results we will + // merge and de-dupe. + fieldSelectors := make([]fields.Selector, 0, 1+len(opts.VerifiedIn)) + warehouseSelector := fields.OneTermEqualSelector("warehouse", w.Name) + if opts.ApprovedFor == "" && len(opts.VerifiedIn) == 0 { + // Just list all Freight resources that originated from the Warehouse + fieldSelectors = append(fieldSelectors, warehouseSelector) + } + if opts.ApprovedFor != "" { + // List all Freight resources that are approved for the specified Stage + fieldSelectors = append( + fieldSelectors, + fields.AndSelectors( + warehouseSelector, + fields.OneTermEqualSelector("approvedFor", opts.ApprovedFor), + ), + ) + } + for _, stage := range opts.VerifiedIn { + // List all Freight resources that are verified in the specified Stage + fieldSelectors = append( + fieldSelectors, + fields.AndSelectors( + warehouseSelector, + fields.OneTermEqualSelector("verifiedIn", stage), + ), + ) + } + + freight := []Freight{} + for _, fs := range fieldSelectors { + res := &FreightList{} + if err := c.List( + ctx, + res, + &client.ListOptions{ + Namespace: w.Namespace, + FieldSelector: fs, + }, + ); err != nil { + return nil, fmt.Errorf( + "error listing Freight for Warehouse %q in namespace %q: %w", + w.Name, + w.Namespace, + err, + ) + } + freight = append(freight, res.Items...) + } + + // Sort and de-dupe + slices.SortFunc(freight, func(lhs, rhs Freight) int { + return strings.Compare(lhs.Name, rhs.Name) + }) + freight = slices.CompactFunc(freight, func(lhs, rhs Freight) bool { + return lhs.Name == rhs.Name + }) + + if len(opts.VerifiedIn) == 0 || opts.VerifiedBefore == nil { + // Nothing left to do + return freight, nil + } + + // Filter out Freight whose soak time has not yet elapsed + filtered := make([]Freight, 0, len(freight)) +freightLoop: + for _, f := range freight { + if opts.ApprovedFor != "" { + if f.IsApprovedFor(opts.ApprovedFor) { + filtered = append(filtered, f) + continue + } + } + for _, ver := range f.Status.VerifiedIn { + if verifiedAt := ver.VerifiedAt; verifiedAt != nil { + if verifiedAt.Time.Before(opts.VerifiedBefore.Time) { + filtered = append(filtered, f) + continue freightLoop + } + } + } + } + return filtered, nil +} diff --git a/api/v1alpha1/warehouse_helpers_test.go b/api/v1alpha1/warehouse_helpers_test.go index a8afc2f28..93d11dd7f 100644 --- a/api/v1alpha1/warehouse_helpers_test.go +++ b/api/v1alpha1/warehouse_helpers_test.go @@ -2,16 +2,51 @@ package v1alpha1 import ( "context" + "errors" "testing" + "time" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" ) +// TODO(krancour): If we move our actual indexers to this package, we can use +// them here instead of duplicating them for the sake of avoiding an import +// cycle. +const warehouseField = "warehouse" + +func warehouseIndexer(obj client.Object) []string { + return []string{obj.(*Freight).Origin.Name} // nolint: forcetypeassert +} + +const approvedField = "approvedFor" + +func approvedForIndexer(obj client.Object) []string { + freight := obj.(*Freight) // nolint: forcetypeassert + var approvedFor []string + for stage := range freight.Status.ApprovedFor { + approvedFor = append(approvedFor, stage) + } + return approvedFor +} + +const verifiedInField = "verifiedIn" + +func verifiedInIndexer(obj client.Object) []string { + freight := obj.(*Freight) // nolint: forcetypeassert + var verifiedIn []string + for stage := range freight.Status.VerifiedIn { + verifiedIn = append(verifiedIn, stage) + } + return verifiedIn +} + func TestGetWarehouse(t *testing.T) { scheme := k8sruntime.NewScheme() require.NoError(t, SchemeBuilder.AddToScheme(scheme)) @@ -62,3 +97,203 @@ func TestGetWarehouse(t *testing.T) { }) } } + +func TestWarehouse_ListFreight(t *testing.T) { + const testProject = "fake-project" + const testWarehouse = "fake-warehouse" + const testStage = "fake-stage" + const testUpstreamStage = "fake-upstream-stage" + + testCases := []struct { + name string + opts *ListWarehouseFreightOptions + objects []client.Object + interceptor interceptor.Funcs + assertions func(*testing.T, []Freight, error) + }{ + { + name: "error listing Freight", + interceptor: interceptor.Funcs{ + List: func( + context.Context, + client.WithWatch, + client.ObjectList, + ...client.ListOption, + ) error { + return errors.New("something went wrong") + }, + }, + assertions: func(t *testing.T, freight []Freight, err error) { + require.ErrorContains(t, err, "error listing Freight") + require.ErrorContains(t, err, "something went wrong") + require.Nil(t, freight) + }, + }, + { + name: "success with no options", + objects: []client.Object{ + &Freight{ // This should be returned + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight", + }, + Origin: FreightOrigin{ + Kind: FreightOriginKindWarehouse, + Name: testWarehouse, + }, + }, + &Freight{ // This should not be returned + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "another-fake-freight", + }, + Origin: FreightOrigin{ + Kind: FreightOriginKindWarehouse, + Name: "wrong-warehouse", + }, + }, + &Freight{ // This should not be returned + ObjectMeta: metav1.ObjectMeta{ + Namespace: "wrong-project", + Name: "another-fake-freight", + }, + Origin: FreightOrigin{ + Kind: FreightOriginKindWarehouse, + Name: testWarehouse, + }, + }, + }, + assertions: func(t *testing.T, freight []Freight, err error) { + require.NoError(t, err) + require.Len(t, freight, 1) + require.Equal(t, testProject, freight[0].Namespace) + require.Equal(t, "fake-freight", freight[0].Name) + }, + }, + { + name: "success with options", + opts: &ListWarehouseFreightOptions{ + ApprovedFor: testStage, + VerifiedIn: []string{testUpstreamStage}, + VerifiedBefore: &metav1.Time{Time: time.Now().Add(-1 * time.Hour)}, + }, + objects: []client.Object{ + &Freight{ // This should not be returned + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight-1", + }, + Origin: FreightOrigin{ + Kind: FreightOriginKindWarehouse, + Name: "wrong-warehouse", + }, + Status: FreightStatus{ + // Doesn't matter that it's approved for the stage, because this is + // the wrong warehouse + ApprovedFor: map[string]ApprovedStage{testStage: {}}, + // Doesn't matter that it's verified upstream, because this is the + // wrong warehouse + VerifiedIn: map[string]VerifiedStage{testUpstreamStage: {}}, + }, + }, + &Freight{ // This should not be returned + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight-2", + }, + Origin: FreightOrigin{ + Kind: FreightOriginKindWarehouse, + Name: testWarehouse, + }, + // This is not approved or verified in any Stages + }, + &Freight{ // This should be returned + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight-3", + }, + Origin: FreightOrigin{ + Kind: FreightOriginKindWarehouse, + Name: testWarehouse, + }, + Status: FreightStatus{ + // This is approved for the Stage + ApprovedFor: map[string]ApprovedStage{testStage: {}}, + }, + }, + &Freight{ // This should not be returned + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight-4", + }, + Origin: FreightOrigin{ + Kind: FreightOriginKindWarehouse, + Name: testWarehouse, + }, + Status: FreightStatus{ + // This is verified in the upstream Stage, but the soak time has not + // yet elapsed + VerifiedIn: map[string]VerifiedStage{ + testUpstreamStage: { + VerifiedAt: ptr.To(metav1.Now()), + }, + }, + }, + }, + &Freight{ // This should be returned + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight-5", + }, + Origin: FreightOrigin{ + Kind: FreightOriginKindWarehouse, + Name: testWarehouse, + }, + Status: FreightStatus{ + // This is verified in the upstream Stage and the soak time has + // elapsed + VerifiedIn: map[string]VerifiedStage{ + testUpstreamStage: { + VerifiedAt: ptr.To(metav1.NewTime(time.Now().Add(-2 * time.Hour))), + }, + }, + }, + }, + }, + assertions: func(t *testing.T, freight []Freight, err error) { + require.NoError(t, err) + require.Len(t, freight, 2) + require.Equal(t, testProject, freight[0].Namespace) + require.Equal(t, "fake-freight-3", freight[0].Name) + require.Equal(t, testProject, freight[1].Namespace) + require.Equal(t, "fake-freight-5", freight[1].Name) + }, + }, + } + + testScheme := k8sruntime.NewScheme() + err := AddToScheme(testScheme) + require.NoError(t, err) + + for _, testCase := range testCases { + c := fake.NewClientBuilder().WithScheme(testScheme). + WithScheme(testScheme). + WithIndex(&Freight{}, warehouseField, warehouseIndexer). + WithIndex(&Freight{}, approvedField, approvedForIndexer). + WithIndex(&Freight{}, verifiedInField, verifiedInIndexer). + WithObjects(testCase.objects...). + WithInterceptorFuncs(testCase.interceptor). + Build() + + t.Run(testCase.name, func(t *testing.T) { + warehouse := &Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-warehouse", + }, + } + freight, err := warehouse.ListFreight(context.Background(), c, testCase.opts) + testCase.assertions(t, freight, err) + }) + } +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 517d1627b..ef0de2f62 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -205,6 +205,25 @@ func (in *ChartSubscription) DeepCopy() *ChartSubscription { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CurrentStage) DeepCopyInto(out *CurrentStage) { + *out = *in + if in.Since != nil { + in, out := &in.Since, &out.Since + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CurrentStage. +func (in *CurrentStage) DeepCopy() *CurrentStage { + if in == nil { + return nil + } + out := new(CurrentStage) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DiscoveredArtifacts) DeepCopyInto(out *DiscoveredArtifacts) { *out = *in @@ -479,6 +498,11 @@ func (in *FreightSources) DeepCopyInto(out *FreightSources) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.RequiredSoakTime != nil { + in, out := &in.RequiredSoakTime, &out.RequiredSoakTime + *out = new(v1.Duration) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FreightSources. @@ -494,6 +518,13 @@ func (in *FreightSources) DeepCopy() *FreightSources { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FreightStatus) DeepCopyInto(out *FreightStatus) { *out = *in + if in.CurrentlyIn != nil { + in, out := &in.CurrentlyIn, &out.CurrentlyIn + *out = make(map[string]CurrentStage, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } if in.VerifiedIn != nil { in, out := &in.VerifiedIn, &out.VerifiedIn *out = make(map[string]VerifiedStage, len(*in)) @@ -1379,6 +1410,11 @@ func (in *VerifiedStage) DeepCopyInto(out *VerifiedStage) { in, out := &in.VerifiedAt, &out.VerifiedAt *out = (*in).DeepCopy() } + if in.LongestCompletedSoak != nil { + in, out := &in.LongestCompletedSoak, &out.LongestCompletedSoak + *out = new(v1.Duration) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VerifiedStage. diff --git a/charts/kargo/resources/crds/kargo.akuity.io_freights.yaml b/charts/kargo/resources/crds/kargo.akuity.io_freights.yaml index 0db83450b..bb72f2155 100644 --- a/charts/kargo/resources/crds/kargo.akuity.io_freights.yaml +++ b/charts/kargo/resources/crds/kargo.akuity.io_freights.yaml @@ -160,7 +160,7 @@ spec: name: description: |- Name is the name of the resource of the kind indicated by the Kind field - from which Freight may originated. + from which Freight may originate. type: string required: - kind @@ -187,11 +187,34 @@ spec: might wish to promote a piece of Freight to a given Stage without transiting the entire pipeline. type: object + currentlyIn: + additionalProperties: + description: CurrentStage reflects a Stage's current use of Freight. + properties: + since: + description: |- + Since is the time at which the Stage most recently started using the + Freight. This can be used to calculate how long the Freight has been in use + by the Stage. + format: date-time + type: string + type: object + description: CurrentlyIn describes the Stages in which this Freight + is currently in use. + type: object verifiedIn: additionalProperties: description: VerifiedStage describes a Stage in which Freight has been verified. properties: + longestSoak: + description: |- + LongestCompletedSoak represents the longest definite time interval wherein + the Freight was in CONTINUOUS use by the Stage. This value is updated as + Freight EXITS the Stage. If the Freight is currently in use by the Stage, + the time elapsed since the Freight ENTERED the Stage is its current soak + time, which may exceed the value of this field. + type: string verifiedAt: description: VerifiedAt is the time at which the Freight was verified in the Stage. diff --git a/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml b/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml index d0589a2d8..72c0ccaaf 100644 --- a/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml +++ b/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml @@ -306,7 +306,7 @@ spec: name: description: |- Name is the name of the resource of the kind indicated by the Kind field - from which Freight may originated. + from which Freight may originate. type: string required: - kind @@ -441,7 +441,7 @@ spec: name: description: |- Name is the name of the resource of the kind indicated by the Kind field - from which Freight may originated. + from which Freight may originate. type: string required: - kind diff --git a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml index ce48b3f19..f10b055aa 100644 --- a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml +++ b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml @@ -201,7 +201,7 @@ spec: name: description: |- Name is the name of the resource of the kind indicated by the Kind field - from which Freight may originated. + from which Freight may originate. type: string required: - kind @@ -219,6 +219,18 @@ spec: the value of the Stages field must be non-empty. i.e. Between the two fields, at least one source must be specified. type: boolean + requiredSoakTime: + description: |- + RequiredSoakTime specifies a minimum duration for which the requested + Freight must have continuously occupied ("soaked in") in an upstream Stage + before becoming available for promotion to this Stage. This is an optional + field. If nil or zero, no soak time is required. Any soak time requirement + is in ADDITION to the requirement that Freight be verified in an upstream + Stage to become available for promotion to this Stage, although a manual + approval for promotion to this Stage will supersede any soak time + requirement. + pattern: ^([0-9]+(\.[0-9]+)?(s|m|h))+$ + type: string stages: description: |- Stages identifies other "upstream" Stages as potential sources of the @@ -493,7 +505,7 @@ spec: name: description: |- Name is the name of the resource of the kind indicated by the Kind field - from which Freight may originated. + from which Freight may originate. type: string required: - kind @@ -634,7 +646,7 @@ spec: name: description: |- Name is the name of the resource of the kind indicated by the Kind field - from which Freight may originated. + from which Freight may originate. type: string required: - kind @@ -771,7 +783,7 @@ spec: name: description: |- Name is the name of the resource of the kind indicated by the Kind field - from which Freight may originated. + from which Freight may originate. type: string required: - kind @@ -1069,7 +1081,7 @@ spec: name: description: |- Name is the name of the resource of the kind indicated by the Kind field - from which Freight may originated. + from which Freight may originate. type: string required: - kind @@ -1310,7 +1322,7 @@ spec: name: description: |- Name is the name of the resource of the kind indicated by the Kind field - from which Freight may originated. + from which Freight may originate. type: string required: - kind @@ -1451,7 +1463,7 @@ spec: name: description: |- Name is the name of the resource of the kind indicated by the Kind field - from which Freight may originated. + from which Freight may originate. type: string required: - kind @@ -1588,7 +1600,7 @@ spec: name: description: |- Name is the name of the resource of the kind indicated by the Kind field - from which Freight may originated. + from which Freight may originate. type: string required: - kind diff --git a/internal/api/approve_freight_v1alpha1.go b/internal/api/approve_freight_v1alpha1.go index 2252e2c40..cedb9145c 100644 --- a/internal/api/approve_freight_v1alpha1.go +++ b/internal/api/approve_freight_v1alpha1.go @@ -4,13 +4,12 @@ import ( "context" "errors" "fmt" + "time" "connectrpc.com/connect" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" kargoapi "github.com/akuity/kargo/api/v1alpha1" @@ -101,18 +100,15 @@ func (s *server) ApproveFreight( return nil, err } - newStatus := *freight.Status.DeepCopy() - if newStatus.ApprovedFor == nil { - newStatus.ApprovedFor = make(map[string]kargoapi.ApprovedStage) - } - - if _, ok := newStatus.ApprovedFor[stageName]; ok { + if freight.IsApprovedFor(stageName) { return &connect.Response[svcv1alpha1.ApproveFreightResponse]{}, nil } - newStatus.ApprovedFor[stageName] = kargoapi.ApprovedStage{ - ApprovedAt: ptr.To(metav1.Now()), + newStatus := *freight.Status.DeepCopy() + if newStatus.ApprovedFor == nil { + newStatus.ApprovedFor = make(map[string]kargoapi.ApprovedStage) } + newStatus.AddApprovedStage(stageName, time.Now()) if err := s.patchFreightStatusFn(ctx, freight, newStatus); err != nil { return nil, fmt.Errorf("patch status: %w", err) diff --git a/internal/api/promote_downstream_v1alpha1.go b/internal/api/promote_downstream_v1alpha1.go index 888194f95..cf2c44891 100644 --- a/internal/api/promote_downstream_v1alpha1.go +++ b/internal/api/promote_downstream_v1alpha1.go @@ -93,24 +93,6 @@ func (s *server) PromoteDownstream( return nil, connect.NewError(connect.CodeNotFound, err) } - var directAllowed bool - for _, req := range stage.Spec.RequestedFreight { - if req.Origin.Equals(&freight.Origin) && req.Sources.Direct { - directAllowed = true - break - } - } - if _, verified := freight.Status.VerifiedIn[stage.Name]; !verified && !directAllowed { - return nil, connect.NewError( - connect.CodeInvalidArgument, - fmt.Errorf( - "Freight %q is not available to Stages down stream from %q", - freightName, - stageName, - ), - ) - } - downstreams, err := s.findDownstreamStagesFn(ctx, stage, freight.Origin) if err != nil { return nil, fmt.Errorf("find downstream stages: %w", err) @@ -134,6 +116,19 @@ func (s *server) PromoteDownstream( } } + for _, downstream := range downstreams { + if !downstream.IsFreightAvailable(freight) { + return nil, connect.NewError( + connect.CodeInvalidArgument, + fmt.Errorf( + "Freight %q is not available to downstream Stage %q", + freight.Name, + downstream.Name, + ), + ) + } + } + promoteErrs := make([]error, 0, len(downstreams)) createdPromos := make([]*kargoapi.Promotion, 0, len(downstreams)) for _, downstream := range downstreams { diff --git a/internal/api/promote_downstream_v1alpha1_test.go b/internal/api/promote_downstream_v1alpha1_test.go index 1c30b91fc..0782f9def 100644 --- a/internal/api/promote_downstream_v1alpha1_test.go +++ b/internal/api/promote_downstream_v1alpha1_test.go @@ -221,7 +221,7 @@ func TestPromoteDownstream(t *testing.T) { }, }, { - name: "Freight not available", + name: "error finding downstream Stages", req: &svcv1alpha1.PromoteDownstreamRequest{ Project: "fake-project", Stage: "fake-stage", @@ -237,6 +237,10 @@ func TestPromoteDownstream(t *testing.T) { types.NamespacedName, ) (*kargoapi.Stage, error) { return &kargoapi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "fake-stage", + }, Spec: testStageSpec, }, nil }, @@ -245,7 +249,20 @@ func TestPromoteDownstream(t *testing.T) { client.Client, string, string, string, ) (*kargoapi.Freight, error) { - return &kargoapi.Freight{}, nil + return &kargoapi.Freight{ + Status: kargoapi.FreightStatus{ + VerifiedIn: map[string]kargoapi.VerifiedStage{ + "fake-stage": {}, + }, + }, + }, nil + }, + findDownstreamStagesFn: func( + context.Context, + *kargoapi.Stage, + kargoapi.FreightOrigin, + ) ([]kargoapi.Stage, error) { + return nil, errors.New("something went wrong") }, }, assertions: func( @@ -255,15 +272,11 @@ func TestPromoteDownstream(t *testing.T) { err error, ) { require.Error(t, err) - var connErr *connect.Error - require.True(t, errors.As(err, &connErr)) - require.Equal(t, connect.CodeInvalidArgument, connErr.Code()) - require.Contains(t, connErr.Message(), "Freight") - require.Contains(t, connErr.Message(), "not available to Stage") + require.Equal(t, "find downstream stages: something went wrong", err.Error()) }, }, { - name: "error finding downstream Stages", + name: "no downstream Stages found", req: &svcv1alpha1.PromoteDownstreamRequest{ Project: "fake-project", Stage: "fake-stage", @@ -304,7 +317,7 @@ func TestPromoteDownstream(t *testing.T) { *kargoapi.Stage, kargoapi.FreightOrigin, ) ([]kargoapi.Stage, error) { - return nil, errors.New("something went wrong") + return nil, nil }, }, assertions: func( @@ -314,11 +327,15 @@ func TestPromoteDownstream(t *testing.T) { err error, ) { require.Error(t, err) - require.Equal(t, "find downstream stages: something went wrong", err.Error()) + var connErr *connect.Error + require.True(t, errors.As(err, &connErr)) + require.Equal(t, connect.CodeNotFound, connErr.Code()) + require.Contains(t, connErr.Message(), "stage") + require.Contains(t, connErr.Message(), "has no downstream stages") }, }, { - name: "no downstream Stages found", + name: "promoting not authorized", req: &svcv1alpha1.PromoteDownstreamRequest{ Project: "fake-project", Stage: "fake-stage", @@ -344,7 +361,9 @@ func TestPromoteDownstream(t *testing.T) { getFreightByNameOrAliasFn: func( context.Context, client.Client, - string, string, string, + string, + string, + string, ) (*kargoapi.Freight, error) { return &kargoapi.Freight{ Status: kargoapi.FreightStatus{ @@ -359,7 +378,16 @@ func TestPromoteDownstream(t *testing.T) { *kargoapi.Stage, kargoapi.FreightOrigin, ) ([]kargoapi.Stage, error) { - return nil, nil + return []kargoapi.Stage{{}}, nil + }, + authorizeFn: func( + context.Context, + string, + schema.GroupVersionResource, + string, + client.ObjectKey, + ) error { + return errors.New("not authorized") }, }, assertions: func( @@ -369,15 +397,11 @@ func TestPromoteDownstream(t *testing.T) { err error, ) { require.Error(t, err) - var connErr *connect.Error - require.True(t, errors.As(err, &connErr)) - require.Equal(t, connect.CodeNotFound, connErr.Code()) - require.Contains(t, connErr.Message(), "stage") - require.Contains(t, connErr.Message(), "has no downstream stages") + require.Equal(t, "not authorized", err.Error()) }, }, { - name: "promoting not authorized", + name: "Freight not available", req: &svcv1alpha1.PromoteDownstreamRequest{ Project: "fake-project", Stage: "fake-stage", @@ -393,27 +417,15 @@ func TestPromoteDownstream(t *testing.T) { types.NamespacedName, ) (*kargoapi.Stage, error) { return &kargoapi.Stage{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "fake-project", - Name: "fake-stage", - }, Spec: testStageSpec, }, nil }, getFreightByNameOrAliasFn: func( context.Context, client.Client, - string, - string, - string, + string, string, string, ) (*kargoapi.Freight, error) { - return &kargoapi.Freight{ - Status: kargoapi.FreightStatus{ - VerifiedIn: map[string]kargoapi.VerifiedStage{ - "fake-stage": {}, - }, - }, - }, nil + return &kargoapi.Freight{}, nil }, findDownstreamStagesFn: func( context.Context, @@ -429,7 +441,7 @@ func TestPromoteDownstream(t *testing.T) { string, client.ObjectKey, ) error { - return errors.New("not authorized") + return nil }, }, assertions: func( @@ -439,7 +451,11 @@ func TestPromoteDownstream(t *testing.T) { err error, ) { require.Error(t, err) - require.Equal(t, "not authorized", err.Error()) + var connErr *connect.Error + require.True(t, errors.As(err, &connErr)) + require.Equal(t, connect.CodeInvalidArgument, connErr.Code()) + require.Contains(t, connErr.Message(), "Freight") + require.Contains(t, connErr.Message(), "is not available to downstream Stage") }, }, { @@ -487,11 +503,11 @@ func TestPromoteDownstream(t *testing.T) { return []kargoapi.Stage{ { Spec: kargoapi.StageSpec{ - PromotionTemplate: &kargoapi.PromotionTemplate{ - Spec: kargoapi.PromotionTemplateSpec{ - Steps: []kargoapi.PromotionStep{{}}, + RequestedFreight: []kargoapi.FreightRequest{{ + Sources: kargoapi.FreightSources{ + Stages: []string{"fake-stage"}, }, - }, + }}, }, }, }, nil @@ -571,11 +587,11 @@ func TestPromoteDownstream(t *testing.T) { return []kargoapi.Stage{ { Spec: kargoapi.StageSpec{ - PromotionTemplate: &kargoapi.PromotionTemplate{ - Spec: kargoapi.PromotionTemplateSpec{ - Steps: []kargoapi.PromotionStep{{}}, + RequestedFreight: []kargoapi.FreightRequest{{ + Sources: kargoapi.FreightSources{ + Stages: []string{"fake-stage"}, }, - }, + }}, }, }, }, nil diff --git a/internal/api/promote_to_stage_v1alpha1.go b/internal/api/promote_to_stage_v1alpha1.go index 310d9cd05..3bb68ab64 100644 --- a/internal/api/promote_to_stage_v1alpha1.go +++ b/internal/api/promote_to_stage_v1alpha1.go @@ -125,6 +125,13 @@ func (s *server) PromoteToStage( }), nil } +func (s *server) isFreightAvailable( + stage *kargoapi.Stage, + freight *kargoapi.Freight, +) bool { + return stage.IsFreightAvailable(freight) +} + func (s *server) recordPromotionCreatedEvent( ctx context.Context, p *kargoapi.Promotion, diff --git a/internal/api/query_freights_v1alpha1.go b/internal/api/query_freights_v1alpha1.go index 72f2c7a1f..e862643d3 100644 --- a/internal/api/query_freights_v1alpha1.go +++ b/internal/api/query_freights_v1alpha1.go @@ -79,12 +79,7 @@ func (s *server) QueryFreight( ), ) } - freight, err = s.getAvailableFreightForStageFn( - ctx, - project, - stageName, - stage.Spec.RequestedFreight, - ) + freight, err = s.getAvailableFreightForStageFn(ctx, stage) if err != nil { return nil, fmt.Errorf("get available freight for stage: %w", err) } @@ -127,79 +122,11 @@ func (s *server) QueryFreight( }), nil } -// getAvailableFreightForStage gets all Freight available to the specified Stage -// for any reason. This includes: -// -// 1. Any Freight from a Warehouse that the Stage subscribes to directly -// 2. Any Freight that is verified in any upstream Stages -// 3. Any Freight that is approved for the Stage func (s *server) getAvailableFreightForStage( ctx context.Context, - project string, - stage string, - freightReqs []kargoapi.FreightRequest, + stage *kargoapi.Stage, ) ([]kargoapi.Freight, error) { - // Find all Warehouses and upstream Stages we need to consider - var warehouses []string - var upstreams []string - for _, req := range freightReqs { - if req.Sources.Direct { - warehouses = append(warehouses, req.Origin.Name) - } - upstreams = append(upstreams, req.Sources.Stages...) - } - // De-dupe the upstreams - slices.Sort(upstreams) - upstreams = slices.Compact(upstreams) - - freightFromWarehouses, err := s.getFreightFromWarehousesFn(ctx, project, warehouses) - if err != nil { - return nil, fmt.Errorf("get freight from warehouses: %w", err) - } - - verifiedFreight, err := s.getVerifiedFreightFn(ctx, project, upstreams) - if err != nil { - return nil, fmt.Errorf("get verified freight: %w", err) - } - - var approvedFreight kargoapi.FreightList - if err = s.listFreightFn( - ctx, - &approvedFreight, - &client.ListOptions{ - Namespace: project, - FieldSelector: fields.OneTermEqualSelector( - indexer.FreightApprovedForStagesField, - stage, - ), - }, - ); err != nil { - return nil, fmt.Errorf( - "error listing Freight approved for Stage %q in namespace %q: %w", - stage, - project, - err, - ) - } - if len(freightFromWarehouses) == 0 && - len(verifiedFreight) == 0 && - len(approvedFreight.Items) == 0 { - return nil, nil - } - - // Concatenate all available Freight - availableFreight := append(freightFromWarehouses, verifiedFreight...) - availableFreight = append(availableFreight, approvedFreight.Items...) - - // De-dupe the available Freight - slices.SortFunc(availableFreight, func(lhs, rhs kargoapi.Freight) int { - return strings.Compare(lhs.Name, rhs.Name) - }) - availableFreight = slices.CompactFunc(availableFreight, func(lhs, rhs kargoapi.Freight) bool { - return lhs.Name == rhs.Name - }) - - return availableFreight, nil + return stage.ListAvailableFreight(ctx, s.client) } func (s *server) getFreightFromWarehouses( diff --git a/internal/api/query_freights_v1alpha1_test.go b/internal/api/query_freights_v1alpha1_test.go index c2a170895..ac3415d73 100644 --- a/internal/api/query_freights_v1alpha1_test.go +++ b/internal/api/query_freights_v1alpha1_test.go @@ -164,9 +164,7 @@ func TestQueryFreight(t *testing.T) { }, getAvailableFreightForStageFn: func( context.Context, - string, - string, - []kargoapi.FreightRequest, + *kargoapi.Stage, ) ([]kargoapi.Freight, error) { return nil, errors.New("something went wrong") }, @@ -360,155 +358,6 @@ func TestQueryFreight(t *testing.T) { } } -func TestGetAvailableFreightForStage(t *testing.T) { - testOrigin := kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "fake-warehouse", - } - testCases := []struct { - name string - reqs []kargoapi.FreightRequest - server *server - assertions func(*testing.T, []kargoapi.Freight, error) - }{ - { - name: "error getting Freight from Warehouse", - reqs: []kargoapi.FreightRequest{ - { - Origin: testOrigin, - Sources: kargoapi.FreightSources{ - Direct: true, - }, - }, - }, - server: &server{ - getFreightFromWarehousesFn: func(context.Context, string, []string) ([]kargoapi.Freight, error) { - return nil, errors.New("something went wrong") - }, - }, - assertions: func(t *testing.T, _ []kargoapi.Freight, err error) { - require.ErrorContains(t, err, "get freight from warehouses") - require.ErrorContains(t, err, "something went wrong") - }, - }, - { - name: "error getting Freight verified in upstream Stages", - reqs: []kargoapi.FreightRequest{ - { - Origin: testOrigin, - Sources: kargoapi.FreightSources{ - Stages: []string{"fake-stage"}, - }, - }, - }, - server: &server{ - getFreightFromWarehousesFn: func(context.Context, string, []string) ([]kargoapi.Freight, error) { - return nil, nil - }, - getVerifiedFreightFn: func(context.Context, string, []string) ([]kargoapi.Freight, error) { - return nil, errors.New("something went wrong") - }, - }, - assertions: func(t *testing.T, _ []kargoapi.Freight, err error) { - require.ErrorContains(t, err, "get verified freight") - require.ErrorContains(t, err, "something went wrong") - }, - }, - { - name: "error getting Freight approved for Stage", - reqs: []kargoapi.FreightRequest{ - { - Origin: testOrigin, - Sources: kargoapi.FreightSources{ - Stages: []string{"fake-stage"}, - }, - }, - }, - server: &server{ - getFreightFromWarehousesFn: func(context.Context, string, []string) ([]kargoapi.Freight, error) { - return nil, nil - }, - getVerifiedFreightFn: func(context.Context, string, []string) ([]kargoapi.Freight, error) { - return nil, nil - }, - listFreightFn: func( - context.Context, - client.ObjectList, - ...client.ListOption, - ) error { - return errors.New("something went wrong") - }, - }, - assertions: func(t *testing.T, _ []kargoapi.Freight, err error) { - require.ErrorContains(t, err, "error listing Freight approved for Stage") - require.ErrorContains(t, err, "something went wrong") - }, - }, - { - name: "success getting available Freight", - reqs: []kargoapi.FreightRequest{ - { - Origin: testOrigin, - Sources: kargoapi.FreightSources{ - Stages: []string{"fake-stage"}, - }, - }, - }, - server: &server{ - getFreightFromWarehousesFn: func(context.Context, string, []string) ([]kargoapi.Freight, error) { - return []kargoapi.Freight{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "fake-freight-from-warehouse", - }, - }, - }, nil - }, - getVerifiedFreightFn: func(context.Context, string, []string) ([]kargoapi.Freight, error) { - return []kargoapi.Freight{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "fake-verified-freight", - }, - }, - }, nil - }, - listFreightFn: func( - _ context.Context, - objList client.ObjectList, - _ ...client.ListOption, - ) error { - freight, ok := objList.(*kargoapi.FreightList) - require.True(t, ok) - freight.Items = []kargoapi.Freight{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "fake-approved-freight", - }, - }, - } - return nil - }, - }, - assertions: func(t *testing.T, freight []kargoapi.Freight, err error) { - require.NoError(t, err) - require.Len(t, freight, 3) - }, - }, - } - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - freight, err := testCase.server.getAvailableFreightForStage( - context.Background(), - "fake-project", - "fake-stage", - testCase.reqs, - ) - testCase.assertions(t, freight, err) - }) - } -} - func TestGetFreightFromWarehouse(t *testing.T) { testCases := []struct { name string diff --git a/internal/api/server.go b/internal/api/server.go index 0a53283f2..f107434eb 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -99,10 +99,8 @@ type server struct { ...client.ListOption, ) error getAvailableFreightForStageFn func( - ctx context.Context, - project string, - stage string, - requestedFreight []kargoapi.FreightRequest, + context.Context, + *kargoapi.Stage, ) ([]kargoapi.Freight, error) getFreightFromWarehousesFn func( ctx context.Context, @@ -173,7 +171,7 @@ func NewServer( s.externalValidateProjectFn = validation.ValidateProject s.getStageFn = kargoapi.GetStage s.getFreightByNameOrAliasFn = kargoapi.GetFreightByNameOrAlias - s.isFreightAvailableFn = kargoapi.IsFreightAvailable + s.isFreightAvailableFn = s.isFreightAvailable s.createPromotionFn = kubeClient.Create s.findDownstreamStagesFn = s.findDownstreamStages s.listFreightFn = kubeClient.List diff --git a/internal/controller/promotions/promotions.go b/internal/controller/promotions/promotions.go index 57014318d..26604c080 100644 --- a/internal/controller/promotions/promotions.go +++ b/internal/controller/promotions/promotions.go @@ -435,7 +435,7 @@ func (r *reconciler) promote( return nil, fmt.Errorf("Freight %q not found in namespace %q", promo.Spec.Freight, promo.Namespace) } - if !kargoapi.IsFreightAvailable(stage, targetFreight) { + if !stage.IsFreightAvailable(targetFreight) { return nil, fmt.Errorf( "Freight %q is not available to Stage %q in namespace %q", promo.Spec.Freight, diff --git a/internal/controller/stages/control_flow_stages.go b/internal/controller/stages/control_flow_stages.go index 108d544e1..bc91284e3 100644 --- a/internal/controller/stages/control_flow_stages.go +++ b/internal/controller/stages/control_flow_stages.go @@ -3,17 +3,12 @@ package stages import ( "context" "fmt" - "slices" - "strings" "time" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/types" kerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/client-go/tools/record" - "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -72,6 +67,15 @@ func (r *ControlFlowStageReconciler) SetupWithManager( return fmt.Errorf("error setting up index for Freight by Warehouse: %w", err) } + if err := sharedIndexer.IndexField( + ctx, + &kargoapi.Freight{}, + indexer.FreightByCurrentStagesField, + indexer.FreightByCurrentStages, + ); err != nil { + return fmt.Errorf("error setting up index for Freight by current Stages: %w", err) + } + // This index is used to find and watch all Freight that have been verified // in a specific Stage (upstream) to which the control flow Stage is the // downstream consumer. @@ -249,14 +253,7 @@ func (r *ControlFlowStageReconciler) reconcile( // Get the available Freight for the Stage. logger.Debug("getting available Freight") - freight, err := r.getAvailableFreight( - ctx, - types.NamespacedName{ - Namespace: stage.Namespace, - Name: stage.Name, - }, - stage.Spec.RequestedFreight, - ) + freight, err := stage.ListAvailableFreight(ctx, r.client) if err != nil { newStatus.Message = err.Error() return newStatus, err @@ -264,14 +261,14 @@ func (r *ControlFlowStageReconciler) reconcile( // If there is new Freight to verify, do so. if len(freight) > 0 { - logger.Debug("found new Freight", "count", len(freight)) - - logger.Debug("verifying Freight") - if err = r.verifyFreight(ctx, stage, freight, startTime, time.Now()); err != nil { + newlyVerified, err := r.markFreightVerifiedForStage(ctx, stage, freight, startTime, time.Now()) + if newlyVerified > 0 { + logger.Debug("verified Freight", "count", newlyVerified) + } + if err != nil { newStatus.Message = err.Error() return newStatus, err } - logger.Debug("verified Freight", "count", len(freight)) } return newStatus, nil @@ -310,115 +307,29 @@ func (r *ControlFlowStageReconciler) initializeStatus(stage *kargoapi.Stage) kar return *newStatus } -// getAvailableFreight returns the list of available Freight for the given -// Stage. Freight is considered available if it can be sourced directly from -// the Warehouse or if it has been verified in upstream Stages. It excludes -// Freight that has already been verified in the given Stage. -func (r *ControlFlowStageReconciler) getAvailableFreight( - ctx context.Context, - stage types.NamespacedName, - requested []kargoapi.FreightRequest, -) ([]kargoapi.Freight, error) { - var availableFreight []kargoapi.Freight - for _, req := range requested { - // Get Freight directly from the Warehouse if allowed. - if req.Sources.Direct && req.Origin.Kind == kargoapi.FreightOriginKindWarehouse { - var directFreight kargoapi.FreightList - if err := r.client.List( - ctx, - &directFreight, - client.InNamespace(stage.Namespace), - client.MatchingFieldsSelector{ - Selector: fields.OneTermEqualSelector(indexer.FreightByWarehouseField, req.Origin.Name), - }, - ); err != nil { - return nil, fmt.Errorf( - "error listing Freight from %q in namespace %q: %w", - req.Origin.String(), - stage.Namespace, - err, - ) - } - - for _, f := range directFreight.Items { - // TODO(hidde): It would be better to use a fields.AndSelectors - // above in combination with a fields.OneTermNotEqualSelector - // to filter out Freight that has already been verified in this - // Stage. - // - // However, the fake client does not support != field selectors, - // and we would need a "real" Kubernetes API server to test it. - // Until we (finally) make use of testenv, this will have to do. - if _, ok := f.Status.VerifiedIn[stage.Name]; ok { - continue - } - availableFreight = append(availableFreight, f) - } - } - - // Get Freight verified in upstream Stages. - for _, upstream := range req.Sources.Stages { - var verifiedFreight kargoapi.FreightList - if err := r.client.List( - ctx, - &verifiedFreight, - client.InNamespace(stage.Namespace), - client.MatchingFieldsSelector{ - Selector: fields.OneTermEqualSelector(indexer.FreightByVerifiedStagesField, upstream), - }, - ); err != nil { - return nil, fmt.Errorf( - "error listing Freight from %q in namespace %q: %w", - upstream, - stage.Namespace, - err, - ) - } - - for _, f := range verifiedFreight.Items { - // TODO(hidde): It would be better to use a fields.AndSelectors - // above in combination with a fields.OneTermNotEqualSelector - // to filter out Freight that has already been verified in this - // Stage. - // - // However, the fake client does not support != field selectors, - // and we would need a "real" Kubernetes API server to test it. - // Until we (finally) make use of testenv, this will have to do. - if _, ok := f.Status.VerifiedIn[stage.Name]; ok { - continue - } - availableFreight = append(availableFreight, f) - } - } - } - - // As the same Freight may be available due to multiple reasons (e.g. direct - // from Warehouse and verified in upstream Stages), we need to deduplicate - // the list. - slices.SortFunc(availableFreight, func(lhs, rhs kargoapi.Freight) int { - return strings.Compare(lhs.Name, rhs.Name) - }) - availableFreight = slices.CompactFunc(availableFreight, func(lhs, rhs kargoapi.Freight) bool { - return lhs.Name == rhs.Name - }) - - return availableFreight, nil -} - -// verifyFreight marks the given Freight as verified in the given Stage. It -// records an event for each Freight that is successfully verified. -func (r *ControlFlowStageReconciler) verifyFreight( +// markFreightVerifiedForStage marks the given Freight as verified in the given +// Stage, unless it already has been. It records an event for each Freight newly +// marked as verified and returns the total number of Freight that were +// marked as verified. +func (r *ControlFlowStageReconciler) markFreightVerifiedForStage( ctx context.Context, stage *kargoapi.Stage, freight []kargoapi.Freight, startTime, finishTime time.Time, -) error { +) (int, error) { logger := logging.LoggerFromContext(ctx) + var newlyVerified int var failures int for _, f := range freight { // Skip Freight that has already been verified in this Stage. - if _, ok := f.Status.VerifiedIn[stage.Name]; ok { + // + // TODO(hidde + krancour): It would be better to filter out Freight that has + // already been verified in this Stage at retrieval time, but the fake + // client does not support != field selectors, so we would need a "real" + // Kubernetes API server to test it. Until we (finally) make use of testenv, + // this will have to do. + if f.IsVerifiedIn(stage.Name) { continue } @@ -427,9 +338,7 @@ func (r *ControlFlowStageReconciler) verifyFreight( if newStatus.VerifiedIn == nil { newStatus.VerifiedIn = make(map[string]kargoapi.VerifiedStage) } - newStatus.VerifiedIn[stage.Name] = kargoapi.VerifiedStage{ - VerifiedAt: ptr.To(metav1.NewTime(finishTime)), - } + newStatus.AddVerifiedStage(stage.Name, finishTime) if err := kubeclient.PatchStatus(ctx, r.client, &f, func(status *kargoapi.FreightStatus) { *status = *newStatus }); err != nil { @@ -444,6 +353,8 @@ func (r *ControlFlowStageReconciler) verifyFreight( continue } + newlyVerified++ + // Record an event for the verification. r.eventRecorder.AnnotatedEventf( stage, @@ -466,9 +377,9 @@ func (r *ControlFlowStageReconciler) verifyFreight( if failures > 0 { // Return an error if any of the verifications failed. // This will cause the Stage to be requeued. - return fmt.Errorf("failed to verify %d Freight", failures) + return newlyVerified, fmt.Errorf("failed to verify %d Freight", failures) } - return nil + return newlyVerified, nil } // handleDelete handles the deletion of the given control flow Stage. It clears diff --git a/internal/controller/stages/control_flow_stages_test.go b/internal/controller/stages/control_flow_stages_test.go index 1c8d40d42..43d652b25 100644 --- a/internal/controller/stages/control_flow_stages_test.go +++ b/internal/controller/stages/control_flow_stages_test.go @@ -27,8 +27,28 @@ import ( ) func TestControlFlowStageReconciler_Reconcile(t *testing.T) { - scheme := runtime.NewScheme() - require.NoError(t, kargoapi.AddToScheme(scheme)) + testProject := "test-project" + const testWarehouseName = "test-warehouse" + testWarehouse := &kargoapi.Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: testWarehouseName, + }, + } + testStageName := "test-stage" + testStage := types.NamespacedName{ + Namespace: testProject, + Name: testStageName, + } + testStageMeta := metav1.ObjectMeta{ + Namespace: testProject, + Name: testStageName, + Finalizers: []string{kargoapi.FinalizerName}, + } + testWarehouseOrigin := kargoapi.FreightOrigin{ + Kind: kargoapi.FreightOriginKindWarehouse, + Name: testWarehouseName, + } tests := []struct { name string @@ -42,7 +62,7 @@ func TestControlFlowStageReconciler_Reconcile(t *testing.T) { name: "stage not found", req: ctrl.Request{ NamespacedName: types.NamespacedName{ - Namespace: "default", + Namespace: testProject, Name: "non-existent", }, }, @@ -53,24 +73,14 @@ func TestControlFlowStageReconciler_Reconcile(t *testing.T) { }, { name: "ignores non-control flow stage", - req: ctrl.Request{ - NamespacedName: types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, - }, + req: ctrl.Request{NamespacedName: testStage}, stage: &kargoapi.Stage{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-stage", - }, + ObjectMeta: testStageMeta, Spec: kargoapi.StageSpec{ // Not a control flow stage PromotionTemplate: &kargoapi.PromotionTemplate{ Spec: kargoapi.PromotionTemplateSpec{ - Steps: []kargoapi.PromotionStep{ - {}, - }, + Steps: []kargoapi.PromotionStep{{}}, }, }, }, @@ -82,16 +92,11 @@ func TestControlFlowStageReconciler_Reconcile(t *testing.T) { }, { name: "handles deletion", - req: ctrl.Request{ - NamespacedName: types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, - }, + req: ctrl.Request{NamespacedName: testStage}, stage: &kargoapi.Stage{ ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-stage", + Namespace: testProject, + Name: testStageName, DeletionTimestamp: &metav1.Time{Time: time.Now()}, Finalizers: []string{kargoapi.FinalizerName}, }, @@ -103,16 +108,11 @@ func TestControlFlowStageReconciler_Reconcile(t *testing.T) { }, { name: "deletion error", - req: ctrl.Request{ - NamespacedName: types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, - }, + req: ctrl.Request{NamespacedName: testStage}, stage: &kargoapi.Stage{ ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-stage", + Namespace: testProject, + Name: testStageName, DeletionTimestamp: &metav1.Time{Time: time.Now()}, Finalizers: []string{kargoapi.FinalizerName}, }, @@ -129,16 +129,11 @@ func TestControlFlowStageReconciler_Reconcile(t *testing.T) { }, { name: "adds finalizer and requeues", - req: ctrl.Request{ - NamespacedName: types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, - }, + req: ctrl.Request{NamespacedName: testStage}, stage: &kargoapi.Stage{ ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-stage", + Namespace: testProject, + Name: testStageName, }, }, assertions: func(t *testing.T, c client.Client, result ctrl.Result, err error) { @@ -147,43 +142,26 @@ func TestControlFlowStageReconciler_Reconcile(t *testing.T) { // Verify finalizer was added stage := &kargoapi.Stage{} - err = c.Get(context.Background(), types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, stage) + err = c.Get(context.Background(), testStage, stage) require.NoError(t, err) assert.Contains(t, stage.Finalizers, kargoapi.FinalizerName) }, }, { name: "reconcile error", - req: ctrl.Request{ - NamespacedName: types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, - }, + req: ctrl.Request{NamespacedName: testStage}, stage: &kargoapi.Stage{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-stage", - Finalizers: []string{kargoapi.FinalizerName}, - }, + ObjectMeta: testStageMeta, Spec: kargoapi.StageSpec{ - RequestedFreight: []kargoapi.FreightRequest{ - { - Sources: kargoapi.FreightSources{ - Direct: true, - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - }, - }, + RequestedFreight: []kargoapi.FreightRequest{{ + Origin: testWarehouseOrigin, + Sources: kargoapi.FreightSources{Direct: true}, + }}, }, }, + objects: []client.Object{testWarehouse}, interceptor: interceptor.Funcs{ + // This will force an error when attempting to list available Freight List: func(context.Context, client.WithWatch, client.ObjectList, ...client.ListOption) error { return fmt.Errorf("something went wrong") }, @@ -194,46 +172,30 @@ func TestControlFlowStageReconciler_Reconcile(t *testing.T) { // Verify error is recorded in status stage := &kargoapi.Stage{} - err = c.Get(context.Background(), types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, stage) + err = c.Get(context.Background(), testStage, stage) require.NoError(t, err) assert.Contains(t, stage.Status.Message, "something went wrong") }, }, { name: "status update error after reconcile error", - req: ctrl.Request{ - NamespacedName: types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, - }, + req: ctrl.Request{NamespacedName: testStage}, stage: &kargoapi.Stage{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-stage", - Finalizers: []string{kargoapi.FinalizerName}, - }, + ObjectMeta: testStageMeta, Spec: kargoapi.StageSpec{ - RequestedFreight: []kargoapi.FreightRequest{ - { - Sources: kargoapi.FreightSources{ - Direct: true, - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - }, - }, + RequestedFreight: []kargoapi.FreightRequest{{ + Origin: testWarehouseOrigin, + Sources: kargoapi.FreightSources{Direct: true}, + }}, }, }, + objects: []client.Object{testWarehouse}, interceptor: interceptor.Funcs{ + // This will force an error when attempting to list available Freight List: func(context.Context, client.WithWatch, client.ObjectList, ...client.ListOption) error { return fmt.Errorf("something went wrong") }, + // This will force an error when attempting to update the Stage status SubResourcePatch: func( context.Context, client.Client, @@ -252,22 +214,11 @@ func TestControlFlowStageReconciler_Reconcile(t *testing.T) { }, }, { - name: "status update error after successful reconcile", - req: ctrl.Request{ - NamespacedName: types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, - }, - stage: &kargoapi.Stage{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-stage", - Finalizers: []string{kargoapi.FinalizerName}, - }, - Spec: kargoapi.StageSpec{}, - }, + name: "status update error after successful reconcile", + req: ctrl.Request{NamespacedName: testStage}, + stage: &kargoapi.Stage{ObjectMeta: testStageMeta}, interceptor: interceptor.Funcs{ + // This will force an error when attempting to update the Stage status SubResourcePatch: func( context.Context, client.Client, @@ -285,31 +236,16 @@ func TestControlFlowStageReconciler_Reconcile(t *testing.T) { }, }, { - name: "successful reconciliation", - req: ctrl.Request{ - NamespacedName: types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, - }, - stage: &kargoapi.Stage{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-stage", - Finalizers: []string{kargoapi.FinalizerName}, - }, - Spec: kargoapi.StageSpec{}, - }, + name: "successful reconciliation", + req: ctrl.Request{NamespacedName: testStage}, + stage: &kargoapi.Stage{ObjectMeta: testStageMeta}, assertions: func(t *testing.T, c client.Client, result ctrl.Result, err error) { require.NoError(t, err) assert.Equal(t, ctrl.Result{}, result) // Verify status was updated stage := &kargoapi.Stage{} - err = c.Get(context.Background(), types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, stage) + err = c.Get(context.Background(), testStage, stage) require.NoError(t, err) assert.Equal(t, kargoapi.StagePhaseNotApplicable, stage.Status.Phase) assert.Empty(t, stage.Status.Message) @@ -317,6 +253,9 @@ func TestControlFlowStageReconciler_Reconcile(t *testing.T) { }, } + scheme := runtime.NewScheme() + require.NoError(t, kargoapi.AddToScheme(scheme)) + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { objects := tt.objects @@ -358,146 +297,132 @@ func TestControlFlowStageReconciler_Reconcile(t *testing.T) { } func TestControlFlowStageReconciler_reconcile(t *testing.T) { - scheme := runtime.NewScheme() - require.NoError(t, kargoapi.AddToScheme(scheme)) - - startTime := time.Now() + const testProject = "test-project" + const testWarehouseName = "test-warehouse" + testWarehouse := &kargoapi.Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: testWarehouseName, + }, + } + const testStage = "test-stage" + testStageMeta := metav1.ObjectMeta{ + Namespace: testProject, + Name: testStage, + } + testWarehouseOrigin := kargoapi.FreightOrigin{ + Kind: kargoapi.FreightOriginKindWarehouse, + Name: testWarehouseName, + } tests := []struct { name string stage *kargoapi.Stage objects []client.Object interceptor interceptor.Funcs - assertions func(*testing.T, kargoapi.StageStatus, error) + assertions func(*testing.T, kargoapi.StageStatus, client.Client, error) }{ { - name: "successful reconciliation with no freight", + name: "no available Freight", stage: &kargoapi.Stage{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-stage", - Namespace: "default", - }, + ObjectMeta: testStageMeta, Spec: kargoapi.StageSpec{ - RequestedFreight: []kargoapi.FreightRequest{ - { - Sources: kargoapi.FreightSources{ - Direct: true, - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - }, - }, + RequestedFreight: []kargoapi.FreightRequest{{ + Sources: kargoapi.FreightSources{Direct: true}, + Origin: testWarehouseOrigin, + }}, }, }, - assertions: func(t *testing.T, status kargoapi.StageStatus, err error) { + objects: []client.Object{ + testWarehouse, + // No Freight exists from this Warehouse + }, + assertions: func(t *testing.T, status kargoapi.StageStatus, _ client.Client, err error) { require.NoError(t, err) assert.Equal(t, kargoapi.StagePhaseNotApplicable, status.Phase) assert.Empty(t, status.Message) }, }, { - name: "successful reconciliation with new freight", + name: "available Freight not yet marked as verified", stage: &kargoapi.Stage{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-stage", - Namespace: "default", - }, + ObjectMeta: testStageMeta, Spec: kargoapi.StageSpec{ - RequestedFreight: []kargoapi.FreightRequest{ - { - Sources: kargoapi.FreightSources{ - Direct: true, - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - }, - }, + RequestedFreight: []kargoapi.FreightRequest{{ + Sources: kargoapi.FreightSources{Direct: true}, + Origin: testWarehouseOrigin, + }}, }, }, objects: []client.Object{ + testWarehouse, &kargoapi.Freight{ ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "freight-1", - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", + Namespace: testProject, + Name: "fake-freight", }, + Origin: testWarehouseOrigin, }, }, - assertions: func(t *testing.T, status kargoapi.StageStatus, err error) { + assertions: func(t *testing.T, status kargoapi.StageStatus, c client.Client, err error) { require.NoError(t, err) assert.Equal(t, kargoapi.StagePhaseNotApplicable, status.Phase) assert.Empty(t, status.Message) + updatedFreight := &kargoapi.Freight{} + err = c.Get( + context.Background(), + types.NamespacedName{ + Namespace: testProject, + Name: "fake-freight", + }, + updatedFreight, + ) + require.NoError(t, err) + assert.Contains(t, updatedFreight.Status.VerifiedIn, testStage) }, }, { - name: "error getting available freight", + name: "error listing available Freight", stage: &kargoapi.Stage{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-stage", - Namespace: "default", - }, + ObjectMeta: testStageMeta, Spec: kargoapi.StageSpec{ - RequestedFreight: []kargoapi.FreightRequest{ - { - Sources: kargoapi.FreightSources{ - Direct: true, - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - }, - }, + RequestedFreight: []kargoapi.FreightRequest{{ + Sources: kargoapi.FreightSources{Direct: true}, + Origin: testWarehouseOrigin, + }}, }, }, interceptor: interceptor.Funcs{ - List: func(context.Context, client.WithWatch, client.ObjectList, ...client.ListOption) error { + // Listing Freight begins with a Get call to the Warehouse, so this + // will force an error + Get: func(context.Context, client.WithWatch, client.ObjectKey, client.Object, ...client.GetOption) error { return fmt.Errorf("something went wrong") }, }, - assertions: func(t *testing.T, status kargoapi.StageStatus, err error) { + assertions: func(t *testing.T, status kargoapi.StageStatus, _ client.Client, err error) { require.ErrorContains(t, err, "something went wrong") assert.Contains(t, status.Message, "something went wrong") }, }, { - name: "error verifying freight", + name: "error marking Freight as verified", stage: &kargoapi.Stage{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-stage", - Namespace: "default", - }, + ObjectMeta: testStageMeta, Spec: kargoapi.StageSpec{ - RequestedFreight: []kargoapi.FreightRequest{ - { - Sources: kargoapi.FreightSources{ - Direct: true, - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - }, - }, + RequestedFreight: []kargoapi.FreightRequest{{ + Sources: kargoapi.FreightSources{Direct: true}, + Origin: testWarehouseOrigin, + }}, }, }, objects: []client.Object{ + testWarehouse, &kargoapi.Freight{ ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "freight-1", - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", + Namespace: testProject, + Name: "fake-freight", }, + Origin: testWarehouseOrigin, }, }, interceptor: interceptor.Funcs{ @@ -512,7 +437,7 @@ func TestControlFlowStageReconciler_reconcile(t *testing.T) { return fmt.Errorf("something went wrong") }, }, - assertions: func(t *testing.T, status kargoapi.StageStatus, err error) { + assertions: func(t *testing.T, status kargoapi.StageStatus, _ client.Client, err error) { require.ErrorContains(t, err, "failed to verify 1 Freight") assert.Contains(t, status.Message, "failed to verify 1 Freight") }, @@ -520,72 +445,63 @@ func TestControlFlowStageReconciler_reconcile(t *testing.T) { { name: "already verified freight", stage: &kargoapi.Stage{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-stage", - Namespace: "default", - }, + ObjectMeta: testStageMeta, Spec: kargoapi.StageSpec{ - RequestedFreight: []kargoapi.FreightRequest{ - { - Sources: kargoapi.FreightSources{ - Direct: true, - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - }, - }, + RequestedFreight: []kargoapi.FreightRequest{{ + Sources: kargoapi.FreightSources{Direct: true}, + Origin: testWarehouseOrigin, + }}, }, }, objects: []client.Object{ + testWarehouse, &kargoapi.Freight{ ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "freight-1", - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", + Namespace: testProject, + Name: "fake-freight", }, + Origin: testWarehouseOrigin, Status: kargoapi.FreightStatus{ VerifiedIn: map[string]kargoapi.VerifiedStage{ - "test-stage": {}, + testStage: {}, }, }, }, }, - assertions: func(t *testing.T, status kargoapi.StageStatus, err error) { + interceptor: interceptor.Funcs{ + // This is intended to force an error if there is an unexpected attempt + // to patch the Freight status, which should not happen if the Freight + // is already marked as verified. + SubResourcePatch: func( + context.Context, + client.Client, + string, + client.Object, + client.Patch, + ...client.SubResourcePatchOption, + ) error { + return fmt.Errorf("something went wrong") + }, + }, + assertions: func(t *testing.T, status kargoapi.StageStatus, _ client.Client, err error) { require.NoError(t, err) assert.Equal(t, kargoapi.StagePhaseNotApplicable, status.Phase) assert.Empty(t, status.Message) }, }, { - name: "handles stage with refresh annotation", + name: "handles refresh annotation", stage: &kargoapi.Stage{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-stage", - Namespace: "default", + Name: testStage, + Namespace: testProject, Annotations: map[string]string{ kargoapi.AnnotationKeyRefresh: "refresh-token", }, }, - Spec: kargoapi.StageSpec{ - RequestedFreight: []kargoapi.FreightRequest{ - { - Sources: kargoapi.FreightSources{ - Direct: true, - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - }, - }, - }, }, - assertions: func(t *testing.T, status kargoapi.StageStatus, err error) { + objects: []client.Object{testWarehouse}, + assertions: func(t *testing.T, status kargoapi.StageStatus, _ client.Client, err error) { require.NoError(t, err) assert.Equal(t, kargoapi.StagePhaseNotApplicable, status.Phase) assert.Equal(t, "refresh-token", status.LastHandledRefresh) @@ -596,21 +512,24 @@ func TestControlFlowStageReconciler_reconcile(t *testing.T) { name: "observes generation on reconciliation", stage: &kargoapi.Stage{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-stage", - Namespace: "default", + Name: testStage, + Namespace: testProject, Generation: 2, }, Status: kargoapi.StageStatus{ ObservedGeneration: 1, }, }, - assertions: func(t *testing.T, status kargoapi.StageStatus, err error) { + assertions: func(t *testing.T, status kargoapi.StageStatus, _ client.Client, err error) { require.NoError(t, err) assert.Equal(t, int64(2), status.ObservedGeneration) }, }, } + scheme := runtime.NewScheme() + require.NoError(t, kargoapi.AddToScheme(scheme)) + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := fake.NewClientBuilder(). @@ -635,8 +554,8 @@ func TestControlFlowStageReconciler_reconcile(t *testing.T) { eventRecorder: fakeevent.NewEventRecorder(10), } - status, err := r.reconcile(context.Background(), tt.stage, startTime) - tt.assertions(t, status, err) + status, err := r.reconcile(context.Background(), tt.stage, time.Now()) + tt.assertions(t, status, c, err) }) } } @@ -711,449 +630,7 @@ func TestControlFlowStageReconciler_initializeStatus(t *testing.T) { } } -func TestControlFlowStageReconciler_getAvailableFreight(t *testing.T) { - scheme := runtime.NewScheme() - require.NoError(t, kargoapi.AddToScheme(scheme)) - - tests := []struct { - name string - stage types.NamespacedName - objects []client.Object - requested []kargoapi.FreightRequest - interceptor interceptor.Funcs - assertions func(*testing.T, []kargoapi.Freight, error) - }{ - { - name: "no freight requests returns empty list", - stage: types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, - requested: []kargoapi.FreightRequest{}, - assertions: func(t *testing.T, got []kargoapi.Freight, err error) { - require.NoError(t, err) - assert.Empty(t, got) - }, - }, - { - name: "direct warehouse freight", - stage: types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, - objects: []client.Object{ - &kargoapi.Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "freight-1", - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - }, - &kargoapi.Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "other-freight-1", - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-2", - }, - }, - &kargoapi.Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "freight-2", - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - }, - }, - requested: []kargoapi.FreightRequest{ - { - Sources: kargoapi.FreightSources{ - Direct: true, - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - }, - }, - assertions: func(t *testing.T, got []kargoapi.Freight, err error) { - require.NoError(t, err) - assert.Len(t, got, 2) - assert.Equal(t, "freight-1", got[0].Name) - assert.Equal(t, "freight-2", got[1].Name) - }, - }, - { - name: "ignores already verified direct warehouse freight", - stage: types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, - objects: []client.Object{ - &kargoapi.Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "freight-1", - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - Status: kargoapi.FreightStatus{ - VerifiedIn: map[string]kargoapi.VerifiedStage{ - "test-stage": {}, - }, - }, - }, - }, - requested: []kargoapi.FreightRequest{ - { - Sources: kargoapi.FreightSources{ - Direct: true, - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - }, - }, - assertions: func(t *testing.T, got []kargoapi.Freight, err error) { - require.NoError(t, err) - assert.Empty(t, got) - }, - }, - { - name: "upstream warehouse freight", - stage: types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, - objects: []client.Object{ - &kargoapi.Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "freight-1", - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - Status: kargoapi.FreightStatus{ - VerifiedIn: map[string]kargoapi.VerifiedStage{ - "upstream-stage": {}, - }, - }, - }, - &kargoapi.Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "other-freight-1", - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - Status: kargoapi.FreightStatus{ - VerifiedIn: map[string]kargoapi.VerifiedStage{ - "other-stage": {}, - }, - }, - }, - &kargoapi.Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "freight-2", - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - Status: kargoapi.FreightStatus{ - VerifiedIn: map[string]kargoapi.VerifiedStage{ - "upstream-stage": {}, - }, - }, - }, - }, - requested: []kargoapi.FreightRequest{ - { - Sources: kargoapi.FreightSources{ - Stages: []string{"upstream-stage"}, - }, - }, - }, - assertions: func(t *testing.T, freights []kargoapi.Freight, err error) { - require.NoError(t, err) - assert.Len(t, freights, 2) - assert.Equal(t, "freight-1", freights[0].Name) - assert.Equal(t, "freight-2", freights[1].Name) - }, - }, - { - name: "ignores already verified upstream warehouse freight", - stage: types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, - objects: []client.Object{ - &kargoapi.Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "freight-1", - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - Status: kargoapi.FreightStatus{ - VerifiedIn: map[string]kargoapi.VerifiedStage{ - "upstream-stage": {}, - "test-stage": {}, - }, - }, - }, - }, - requested: []kargoapi.FreightRequest{ - { - Sources: kargoapi.FreightSources{ - Stages: []string{"upstream-stage"}, - }, - }, - }, - assertions: func(t *testing.T, freights []kargoapi.Freight, err error) { - require.NoError(t, err) - assert.Empty(t, freights) - }, - }, - { - name: "multiple freight requests", - stage: types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, - objects: []client.Object{ - &kargoapi.Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "direct-freight-1", - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - }, - &kargoapi.Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "upstream-freight-1", - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-2", - }, - Status: kargoapi.FreightStatus{ - VerifiedIn: map[string]kargoapi.VerifiedStage{ - "upstream-stage": {}, - }, - }, - }, - &kargoapi.Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "upstream-freight-2", - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-2", - }, - Status: kargoapi.FreightStatus{ - VerifiedIn: map[string]kargoapi.VerifiedStage{ - "upstream-stage": {}, - }, - }, - }, - }, - requested: []kargoapi.FreightRequest{ - { - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - Sources: kargoapi.FreightSources{ - Direct: true, - }, - }, - { - Sources: kargoapi.FreightSources{ - Stages: []string{"upstream-stage"}, - }, - }, - }, - assertions: func(t *testing.T, freights []kargoapi.Freight, err error) { - require.NoError(t, err) - assert.Len(t, freights, 3) - assert.Equal(t, "direct-freight-1", freights[0].Name) - assert.Equal(t, "upstream-freight-1", freights[1].Name) - assert.Equal(t, "upstream-freight-2", freights[2].Name) - }, - }, - { - name: "deduplicates freight", - stage: types.NamespacedName{ - Namespace: "default", - }, - objects: []client.Object{ - &kargoapi.Freight{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "freight-1", - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - Status: kargoapi.FreightStatus{ - VerifiedIn: map[string]kargoapi.VerifiedStage{ - "upstream-stage": {}, - }, - }, - }, - }, - requested: []kargoapi.FreightRequest{ - { - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - Sources: kargoapi.FreightSources{ - Direct: true, - Stages: []string{"upstream-stage"}, - }, - }, - }, - assertions: func(t *testing.T, freights []kargoapi.Freight, err error) { - require.NoError(t, err) - assert.Len(t, freights, 1) - assert.Equal(t, "freight-1", freights[0].Name) - }, - }, - { - name: "warehouse list error", - stage: types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, - requested: []kargoapi.FreightRequest{ - { - Sources: kargoapi.FreightSources{ - Direct: true, - }, - Origin: kargoapi.FreightOrigin{ - Kind: kargoapi.FreightOriginKindWarehouse, - Name: "warehouse-1", - }, - }, - }, - interceptor: interceptor.Funcs{ - List: func( - ctx context.Context, - c client.WithWatch, - list client.ObjectList, - opts ...client.ListOption, - ) error { - listOpts := &client.ListOptions{} - for _, opt := range opts { - opt.ApplyToList(listOpts) - } - - switch { - case strings.Contains(listOpts.FieldSelector.String(), indexer.FreightByWarehouseField): - return fmt.Errorf("something went wrong") - default: - return c.List(ctx, list, opts...) - } - }, - }, - assertions: func(t *testing.T, got []kargoapi.Freight, err error) { - require.ErrorContains(t, err, "something went wrong") - assert.Nil(t, got) - }, - }, - { - name: "stage list error", - stage: types.NamespacedName{ - Namespace: "default", - Name: "test-stage", - }, - requested: []kargoapi.FreightRequest{ - { - Sources: kargoapi.FreightSources{ - Stages: []string{"upstream-stage"}, - }, - }, - }, - interceptor: interceptor.Funcs{ - List: func( - ctx context.Context, - c client.WithWatch, - list client.ObjectList, - opts ...client.ListOption, - ) error { - listOpts := &client.ListOptions{} - for _, opt := range opts { - opt.ApplyToList(listOpts) - } - - switch { - case strings.Contains(listOpts.FieldSelector.String(), indexer.FreightByVerifiedStagesField): - return fmt.Errorf("something went wrong") - default: - return c.List(ctx, list, opts...) - } - }, - }, - assertions: func(t *testing.T, got []kargoapi.Freight, err error) { - require.ErrorContains(t, err, "something went wrong") - assert.Nil(t, got) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := fake.NewClientBuilder(). - WithScheme(scheme). - WithObjects(tt.objects...). - WithIndex( - &kargoapi.Freight{}, - indexer.FreightByWarehouseField, - indexer.FreightByWarehouse, - ). - WithIndex( - &kargoapi.Freight{}, - indexer.FreightByVerifiedStagesField, - indexer.FreightByVerifiedStages, - ). - WithInterceptorFuncs(tt.interceptor). - Build() - r := &ControlFlowStageReconciler{ - client: c, - } - - got, err := r.getAvailableFreight(context.Background(), tt.stage, tt.requested) - tt.assertions(t, got, err) - }) - } -} - -func TestControlFlowStageReconciler_verifyFreight(t *testing.T) { +func TestControlFlowStageReconciler_markFreightVerifiedForStage(t *testing.T) { scheme := runtime.NewScheme() require.NoError(t, kargoapi.AddToScheme(scheme)) @@ -1420,12 +897,8 @@ func TestControlFlowStageReconciler_verifyFreight(t *testing.T) { eventRecorder: recorder, } - tt.assertions( - t, - c, - recorder, - r.verifyFreight(context.Background(), tt.stage, tt.freight, tt.startTime, tt.finishTime), - ) + _, err := r.markFreightVerifiedForStage(context.Background(), tt.stage, tt.freight, tt.startTime, tt.finishTime) + tt.assertions(t, c, recorder, err) }) } } diff --git a/internal/controller/stages/event_handlers.go b/internal/controller/stages/event_handlers.go index b3588bfc8..67acc5e1d 100644 --- a/internal/controller/stages/event_handlers.go +++ b/internal/controller/stages/event_handlers.go @@ -133,7 +133,7 @@ func (v *downstreamStageEnqueuer[T]) Update( func getNewlyVerifiedStages(existing, updated *kargoapi.Freight) []string { var stages []string for stage := range updated.Status.VerifiedIn { - if _, ok := existing.Status.VerifiedIn[stage]; !ok { + if !existing.IsVerifiedIn(stage) { stages = append(stages, stage) } } @@ -211,7 +211,7 @@ func (a *stageEnqueuerForApprovedFreight[T]) Update( func getNewlyApprovedStages(existing, updated *kargoapi.Freight) []string { var stages []string for stage := range updated.Status.ApprovedFor { - if _, ok := existing.Status.ApprovedFor[stage]; !ok { + if !existing.IsApprovedFor(stage) { stages = append(stages, stage) } } diff --git a/internal/controller/stages/regular_stages.go b/internal/controller/stages/regular_stages.go index 1213cae4d..e894c84e6 100644 --- a/internal/controller/stages/regular_stages.go +++ b/internal/controller/stages/regular_stages.go @@ -398,6 +398,15 @@ func (r *RegularStageReconciler) reconcile( return status, err }, }, + { + name: "syncing Freight", + reconcile: func() (kargoapi.StageStatus, error) { + if err := r.syncFreight(ctx, stage); err != nil { + return stage.Status, fmt.Errorf("failed to sync Freight: %w", err) + } + return stage.Status, nil + }, + }, { name: "assessing health", reconcile: func() (kargoapi.StageStatus, error) { @@ -782,6 +791,77 @@ func (r *RegularStageReconciler) assessHealth(ctx context.Context, stage *kargoa return newStatus } +// syncFreight ensures that all Freight statuses accurately reflect whether they +// are currently in use by the Stage. +func (r *RegularStageReconciler) syncFreight(ctx context.Context, stage *kargoapi.Stage) error { + // Get the Stage's current FreightCollection. + curFreight := stage.Status.FreightHistory.Current() + // Find all Freight that think they're currently in use by this Stage. + var freight []kargoapi.Freight + freight, err := kargoapi.ListFreightByCurrentStage(ctx, r.client, stage) + if err != nil { + return err + } + // Step through all the Freight that think they're currently used by this + // Stage and, if they're not, patch their status to accurately reflect that. + for _, f := range freight { + if !curFreight.Includes(f.Name) { + newStatus := f.Status.DeepCopy() + delete(newStatus.CurrentlyIn, stage.Name) + if err := kubeclient.PatchStatus(ctx, r.client, &f, func(status *kargoapi.FreightStatus) { + *status = *newStatus + }); err != nil { + return fmt.Errorf( + "error patching status of Freight %q in namespace %q: %w", + f.Name, f.Namespace, err, + ) + } + } + } + // Iterate over every piece of Freight that the Stage is actually using to + // make sure that their status accurately reflects that. + // + // This is the timestamp we'll use to track when the Freight came into use + // by the Stage. There are edge cases where this won't be perfectly accurate, + // but it's close enough, especially given that we use it for calculating + // "soak time," which requires a certain MINIMUM amount of time to pass. Any + // inaccuracy in the timestamp only means the Freight has actually "soaked" + // LONGER than what we calculate. + now := time.Now() + for _, fr := range curFreight.References() { + f, err := kargoapi.GetFreight( + ctx, + r.client, + types.NamespacedName{ + Namespace: stage.Namespace, + Name: fr.Name, + }, + ) + if err != nil { + return fmt.Errorf( + "error getting Freight %q in namespace %q: %w", + fr.Name, stage.Namespace, err, + ) + } + if f == nil { + return fmt.Errorf("Freight %q not found in namespace %q", fr.Name, stage.Namespace) + } + if !f.IsCurrentlyIn(stage.Name) { + newStatus := f.Status.DeepCopy() + newStatus.AddCurrentStage(stage.Name, now) + if err = kubeclient.PatchStatus(ctx, r.client, f, func(status *kargoapi.FreightStatus) { + *status = *newStatus + }); err != nil { + return fmt.Errorf( + "error patching status of Freight %q in namespace %q: %w", + f.Name, f.Namespace, err, + ) + } + } + } + return nil +} + // verifyStageFreight verifies the current Freight of a Stage. If the Stage has // no current Freight, or the Freight has already been verified, then no action // is taken. If the Freight has not been verified yet, then a new verification @@ -1039,7 +1119,7 @@ func (r *RegularStageReconciler) markFreightVerifiedForStage( // If the Freight has already been verified, then there is no need to // verify it again. - if _, ok := freight.Status.VerifiedIn[stage.Name]; ok { + if freight.IsVerifiedIn(stage.Name) { logger.Debug("Freight has already been verified in Stage") continue } @@ -1049,9 +1129,7 @@ func (r *RegularStageReconciler) markFreightVerifiedForStage( if status.VerifiedIn == nil { status.VerifiedIn = make(map[string]kargoapi.VerifiedStage) } - status.VerifiedIn[stage.Name] = kargoapi.VerifiedStage{ - VerifiedAt: curFreight.VerificationHistory.Current().FinishTime.DeepCopy(), - } + status.AddVerifiedStage(stage.Name, curFreight.VerificationHistory.Current().FinishTime.Time) }); err != nil { return newStatus, fmt.Errorf( "error marking Freight %q as verified in Stage: %w", @@ -1514,7 +1592,7 @@ func (r *RegularStageReconciler) autoPromoteFreight( } // Retrieve promotable Freight for the Stage. - promotableFreight, err := r.getPromotableFreight(ctx, stageRef, stage.Spec.RequestedFreight) + promotableFreight, err := r.getPromotableFreight(ctx, stage) if err != nil { return newStatus, err } @@ -1634,110 +1712,28 @@ func (r *RegularStageReconciler) autoPromotionAllowed( return false, nil } -// getPromotableFreight retrieves promotable Freight for a Stage based on the -// requested Freight in the Stage specification. +// getPromotableFreight retrieves a map of []Freight promotable to the specified +// Stage, indexed by origin. func (r *RegularStageReconciler) getPromotableFreight( ctx context.Context, - stage types.NamespacedName, - requested []kargoapi.FreightRequest, + stage *kargoapi.Stage, ) (map[string][]kargoapi.Freight, error) { - var promotableFreight = make(map[string][]kargoapi.Freight) + availableFreight, err := stage.ListAvailableFreight(ctx, r.client) + if err != nil { + return nil, fmt.Errorf( + "error listing available Freight for Stage %q: %w", + stage.Name, err, + ) + } - for _, req := range requested { - originID := req.Origin.String() + var promotableFreight = make(map[string][]kargoapi.Freight) + for _, freight := range availableFreight { + originID := freight.Origin.String() if _, ok := promotableFreight[originID]; !ok { - promotableFreight[originID] = nil + promotableFreight[originID] = []kargoapi.Freight{freight} + } else { + promotableFreight[originID] = append(promotableFreight[originID], freight) } - - // Get Freight directly from the Warehouse if allowed. - if req.Origin.Kind == kargoapi.FreightOriginKindWarehouse && req.Sources.Direct { - var directFreight kargoapi.FreightList - if err := r.client.List( - ctx, - &directFreight, - client.InNamespace(stage.Namespace), - client.MatchingFieldsSelector{ - Selector: fields.OneTermEqualSelector(indexer.FreightByWarehouseField, req.Origin.Name), - }, - ); err != nil { - return nil, fmt.Errorf( - "error listing Freight from %q in namespace %q: %w", - originID, - stage.Namespace, - err, - ) - } - - promotableFreight[originID] = append(promotableFreight[originID], directFreight.Items...) - - // If we allow direct Freight, we do not need to look for Freight - // from other sources. Continue to the next requested Freight. - continue - } - - // Get Freight verified in upstream Stages. - for _, upstream := range req.Sources.Stages { - var verifiedFreight kargoapi.FreightList - if err := r.client.List( - ctx, - &verifiedFreight, - client.InNamespace(stage.Namespace), - client.MatchingFieldsSelector{ - Selector: fields.OneTermEqualSelector(indexer.FreightByVerifiedStagesField, upstream), - }, - ); err != nil { - return nil, fmt.Errorf( - "error listing Freight from %q in namespace %q: %w", - upstream, - stage.Namespace, - err, - ) - } - - promotableFreight[originID] = append(promotableFreight[originID], verifiedFreight.Items...) - } - - // Get Freight approved for the Stage. - var approvedFreight kargoapi.FreightList - if err := r.client.List( - ctx, - &approvedFreight, - client.InNamespace(stage.Namespace), - client.MatchingFieldsSelector{ - Selector: fields.AndSelectors( - // TODO(hidde): once we support more Freight origin - // kinds, we need to adjust this. - fields.OneTermEqualSelector( - indexer.FreightByWarehouseField, - req.Origin.Name, - ), - fields.OneTermEqualSelector( - indexer.FreightApprovedForStagesField, - stage.Name, - ), - ), - }, - ); err != nil { - return nil, fmt.Errorf( - "error listing Freight approved for Stage %q in namespace %q: %w", - stage.Name, - stage.Namespace, - err, - ) - } - promotableFreight[originID] = append(promotableFreight[originID], approvedFreight.Items...) - } - - // As the same Freight may be available due to multiple reasons (e.g. - // verified in upstream Stages and approved), we need to deduplicate - // the list. - for origin := range promotableFreight { - slices.SortFunc(promotableFreight[origin], func(lhs, rhs kargoapi.Freight) int { - return strings.Compare(lhs.Name, rhs.Name) - }) - promotableFreight[origin] = slices.CompactFunc(promotableFreight[origin], func(lhs, rhs kargoapi.Freight) bool { - return lhs.Name == rhs.Name - }) } return promotableFreight, nil diff --git a/internal/controller/stages/regular_stages_test.go b/internal/controller/stages/regular_stages_test.go index 0d87a194b..21c2bb31d 100644 --- a/internal/controller/stages/regular_stages_test.go +++ b/internal/controller/stages/regular_stages_test.go @@ -364,6 +364,11 @@ func TestRegularStageReconciler_Reconcile(t *testing.T) { indexer.FreightByWarehouseField, indexer.FreightByWarehouse, ). + WithIndex( + &kargoapi.Freight{}, + indexer.FreightByCurrentStagesField, + indexer.FreightByCurrentStages, + ). WithIndex( &kargoapi.Freight{}, indexer.FreightByVerifiedStagesField, @@ -498,6 +503,11 @@ func TestRegularStagesReconciler_reconcile(t *testing.T) { indexer.FreightByWarehouseField, indexer.FreightByWarehouse, ). + WithIndex( + &kargoapi.Freight{}, + indexer.FreightByCurrentStagesField, + indexer.FreightByCurrentStages, + ). WithIndex( &kargoapi.Freight{}, indexer.FreightByVerifiedStagesField, @@ -1233,6 +1243,150 @@ func TestRegularStageReconciler_syncPromotions(t *testing.T) { } } +func TestRegularStageReconciler_syncFreight(t *testing.T) { + testProject := "fake-project" + + testStage := &kargoapi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-stage", + }, + Status: kargoapi.StageStatus{ + FreightHistory: kargoapi.FreightHistory{{ + Freight: map[string]kargoapi.FreightReference{ + "fake-warehouse-1": {Name: "fake-freight-1"}, + "fake-warehouse-2": {Name: "fake-freight-2"}, + }, + }}, + }, + } + + testCases := []struct { + name string + objects []client.Object + interceptor interceptor.Funcs + assertions func(*testing.T, client.Client, error) + }{ + { + name: "error listing Freight", + interceptor: interceptor.Funcs{ + List: func(context.Context, client.WithWatch, client.ObjectList, ...client.ListOption) error { + return fmt.Errorf("something went wrong") + }, + }, + assertions: func(t *testing.T, _ client.Client, err error) { + require.Error(t, err) + require.Contains(t, err.Error(), "error listing Freight in namespace") + require.Contains(t, err.Error(), "something went wrong") + }, + }, + { + name: "error getting Freight", + interceptor: interceptor.Funcs{ + Get: func(context.Context, client.WithWatch, client.ObjectKey, client.Object, ...client.GetOption) error { + return fmt.Errorf("something went wrong") + }, + }, + assertions: func(t *testing.T, _ client.Client, err error) { + require.Error(t, err) + require.Contains(t, err.Error(), "error getting Freight") + require.Contains(t, err.Error(), "something went wrong") + }, + }, + { + name: "successful sync", + objects: []client.Object{ + &kargoapi.Freight{ // The Stage is using this, but the Freight doesn't know it. + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight-1", + }, + }, + &kargoapi.Freight{ // The Stage is using this, but the Freight doesn't know it. + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight-2", + }, + }, + &kargoapi.Freight{ // The Freight thinks the Stage is using this, but it's not. + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight-3", + }, + Status: kargoapi.FreightStatus{ + CurrentlyIn: map[string]kargoapi.CurrentStage{testStage.Name: {}}, + }, + }, + &kargoapi.Freight{ // The Freight thinks the Stage is using this, but it's not. + ObjectMeta: metav1.ObjectMeta{ + Namespace: testProject, + Name: "fake-freight-4", + }, + Status: kargoapi.FreightStatus{ + CurrentlyIn: map[string]kargoapi.CurrentStage{testStage.Name: {}}, + }, + }, + }, + assertions: func(t *testing.T, c client.Client, err error) { + require.NoError(t, err) + freight := &kargoapi.Freight{} + err = c.Get( + context.Background(), + types.NamespacedName{Namespace: testProject, Name: "fake-freight-1"}, + freight, + ) + require.NoError(t, err) + require.Contains(t, freight.Status.CurrentlyIn, testStage.Name) + err = c.Get( + context.Background(), + types.NamespacedName{Namespace: testProject, Name: "fake-freight-2"}, + freight, + ) + require.NoError(t, err) + require.Contains(t, freight.Status.CurrentlyIn, testStage.Name) + err = c.Get( + context.Background(), + types.NamespacedName{Namespace: testProject, Name: "fake-freight-3"}, + freight, + ) + require.NoError(t, err) + require.NotContains(t, freight.Status.CurrentlyIn, testStage.Name) + err = c.Get( + context.Background(), + types.NamespacedName{Namespace: testProject, Name: "fake-freight-4"}, + freight, + ) + require.NoError(t, err) + require.NotContains(t, freight.Status.CurrentlyIn, testStage.Name) + }, + }, + } + + scheme := runtime.NewScheme() + require.NoError(t, kargoapi.AddToScheme(scheme)) + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + c := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(testCase.objects...). + WithIndex( + &kargoapi.Freight{}, + indexer.FreightByCurrentStagesField, + indexer.FreightByCurrentStages, + ). + WithStatusSubresource(&kargoapi.Stage{}, &kargoapi.Freight{}). + WithInterceptorFuncs(testCase.interceptor). + Build() + + r := &RegularStageReconciler{client: c} + + err := r.syncFreight(context.Background(), testStage) + testCase.assertions(t, c, err) + }) + } +} + func TestRegularStageReconciler_assessHealth(t *testing.T) { scheme := runtime.NewScheme() require.NoError(t, kargoapi.AddToScheme(scheme)) @@ -2680,7 +2834,8 @@ func TestRegularStageReconciler_markFreightVerifiedForStage(t *testing.T) { }, VerificationHistory: []kargoapi.VerificationInfo{ { - Phase: kargoapi.VerificationPhaseSuccessful, + Phase: kargoapi.VerificationPhaseSuccessful, + FinishTime: &metav1.Time{Time: endTime.Time}, }, }, }, @@ -4358,6 +4513,12 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) { }, }, }, + &kargoapi.Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "test-warehouse", + }, + }, &kargoapi.Freight{ ObjectMeta: metav1.ObjectMeta{ Namespace: "fake-project", @@ -4421,7 +4582,7 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) { FreightHistory: kargoapi.FreightHistory{ { Freight: map[string]kargoapi.FreightReference{ - "test-warehouse": {Name: "test-freight-1"}, + "Warehouse/test-warehouse": {Name: "test-freight-1"}, }, }, }, @@ -4441,12 +4602,22 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) { }, }, }, + &kargoapi.Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "test-warehouse", + }, + }, &kargoapi.Freight{ ObjectMeta: metav1.ObjectMeta{ Namespace: "fake-project", Name: "test-freight-1", CreationTimestamp: metav1.Time{Time: now}, }, + Origin: kargoapi.FreightOrigin{ + Kind: kargoapi.FreightOriginKindWarehouse, + Name: "test-warehouse", + }, }, }, assertions: func( @@ -4499,12 +4670,22 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) { }, }, }, + &kargoapi.Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "test-warehouse", + }, + }, &kargoapi.Freight{ ObjectMeta: metav1.ObjectMeta{ Namespace: "fake-project", Name: "test-freight-1", CreationTimestamp: metav1.Time{Time: now}, }, + Origin: kargoapi.FreightOrigin{ + Kind: kargoapi.FreightOriginKindWarehouse, + Name: "test-warehouse", + }, }, &kargoapi.Promotion{ ObjectMeta: metav1.ObjectMeta{ @@ -4571,12 +4752,22 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) { }, }, }, + &kargoapi.Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "test-warehouse", + }, + }, &kargoapi.Freight{ ObjectMeta: metav1.ObjectMeta{ Namespace: "fake-project", Name: "test-freight-1", CreationTimestamp: metav1.Time{Time: now}, }, + Origin: kargoapi.FreightOrigin{ + Kind: kargoapi.FreightOriginKindWarehouse, + Name: "test-warehouse", + }, Status: kargoapi.FreightStatus{ VerifiedIn: map[string]kargoapi.VerifiedStage{ "upstream-stage": {}, @@ -4600,6 +4791,117 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) { assert.Equal(t, "test-freight-1", promoList.Items[0].Spec.Freight) }, }, + { + name: "handles verified freight from upstream stages with verification duration requirement", + stage: &kargoapi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "test-stage", + }, + Spec: kargoapi.StageSpec{ + RequestedFreight: []kargoapi.FreightRequest{ + { + Origin: kargoapi.FreightOrigin{ + Kind: kargoapi.FreightOriginKindWarehouse, + Name: "test-warehouse", + }, + Sources: kargoapi.FreightSources{ + Stages: []string{"upstream-stage"}, + RequiredSoakTime: &metav1.Duration{Duration: time.Hour}, + }, + }, + }, + }, + }, + objects: []client.Object{ + &kargoapi.Project{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-project", + }, + Spec: &kargoapi.ProjectSpec{ + PromotionPolicies: []kargoapi.PromotionPolicy{ + { + Stage: "test-stage", + AutoPromotionEnabled: true, + }, + }, + }, + }, + &kargoapi.Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "test-warehouse", + }, + }, + &kargoapi.Freight{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "test-freight-1", + CreationTimestamp: metav1.Time{Time: now}, + }, + Origin: kargoapi.FreightOrigin{ + Kind: kargoapi.FreightOriginKindWarehouse, + Name: "test-warehouse", + }, + Status: kargoapi.FreightStatus{ + VerifiedIn: map[string]kargoapi.VerifiedStage{ + // Ignored because it does not have a timestamp. + "upstream-stage": {}, + }, + }, + }, + &kargoapi.Freight{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "test-freight-2", + CreationTimestamp: metav1.Time{Time: now.Add(-2 * time.Hour)}, + }, + Origin: kargoapi.FreightOrigin{ + Kind: kargoapi.FreightOriginKindWarehouse, + Name: "test-warehouse", + }, + Status: kargoapi.FreightStatus{ + VerifiedIn: map[string]kargoapi.VerifiedStage{ + "upstream-stage": { + // Should be selected because it was verified + // after the required duration. + VerifiedAt: &metav1.Time{Time: now.Add(-2 * time.Hour)}, + }, + }, + }, + }, + &kargoapi.Freight{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "test-freight-3", + CreationTimestamp: metav1.Time{Time: now.Add(-40 * time.Minute)}, + }, + Status: kargoapi.FreightStatus{ + VerifiedIn: map[string]kargoapi.VerifiedStage{ + "upstream-stage": { + // Should be ignored because it is too recent. + VerifiedAt: &metav1.Time{Time: now.Add(-39 * time.Minute)}, + }, + }, + }, + }, + }, + assertions: func( + t *testing.T, + _ *fakeevent.EventRecorder, + c client.Client, + _ kargoapi.StageStatus, + err error, + ) { + require.NoError(t, err) + + // Verify promotion was created + promoList := &kargoapi.PromotionList{} + require.NoError(t, c.List(context.Background(), promoList, client.InNamespace("fake-project"))) + require.Len(t, promoList.Items, 1) + assert.Equal(t, "test-freight-2", promoList.Items[0].Spec.Freight) + }, + }, { name: "handles freight approved for stage", stage: &kargoapi.Stage{ @@ -4632,6 +4934,12 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) { }, }, }, + &kargoapi.Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "test-warehouse", + }, + }, &kargoapi.Freight{ ObjectMeta: metav1.ObjectMeta{ Namespace: "fake-project", @@ -4709,6 +5017,18 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) { }, }, }, + &kargoapi.Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "warehouse-1", + }, + }, + &kargoapi.Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "warehouse-2", + }, + }, &kargoapi.Freight{ ObjectMeta: metav1.ObjectMeta{ Namespace: "fake-project", @@ -4793,6 +5113,12 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) { }, }, }, + &kargoapi.Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "test-warehouse", + }, + }, &kargoapi.Freight{ ObjectMeta: metav1.ObjectMeta{ Namespace: "fake-project", @@ -4858,13 +5184,22 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) { }, }, }, + &kargoapi.Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "test-warehouse", + }, + }, &kargoapi.Freight{ ObjectMeta: metav1.ObjectMeta{ Namespace: "fake-project", Name: "test-freight", CreationTimestamp: metav1.Time{Time: now}, }, - + Origin: kargoapi.FreightOrigin{ + Kind: kargoapi.FreightOriginKindWarehouse, + Name: "test-warehouse", + }, Status: kargoapi.FreightStatus{ VerifiedIn: map[string]kargoapi.VerifiedStage{ "upstream-stage": {}, @@ -4925,6 +5260,12 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) { }, }, }, + &kargoapi.Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "test-warehouse", + }, + }, &kargoapi.Freight{ ObjectMeta: metav1.ObjectMeta{ Namespace: "fake-project", diff --git a/internal/indexer/indexer.go b/internal/indexer/indexer.go index 6a134b6bd..52f38a3cc 100644 --- a/internal/indexer/indexer.go +++ b/internal/indexer/indexer.go @@ -22,9 +22,10 @@ import ( const ( EventsByInvolvedObjectAPIGroupField = "involvedObject.apiGroup" + FreightByWarehouseField = "warehouse" + FreightByCurrentStagesField = "currentlyIn" FreightByVerifiedStagesField = "verifiedIn" FreightApprovedForStagesField = "approvedFor" - FreightByWarehouseField = "warehouse" PromotionsByStageAndFreightField = "stageAndFreight" PromotionsByStageField = "stage" @@ -294,6 +295,23 @@ func FreightByWarehouse(obj client.Object) []string { return nil } +// FreightByCurrentStages is a client.IndexerFunc that indexes Freight by the +// Stages in which it is currently in use. +func FreightByCurrentStages(obj client.Object) []string { + freight, ok := obj.(*kargoapi.Freight) + if !ok { + return nil + } + + currentStages := make([]string, len(freight.Status.CurrentlyIn)) + var i int + for stage := range freight.Status.CurrentlyIn { + currentStages[i] = stage + i++ + } + return currentStages +} + // FreightByVerifiedStages is a client.IndexerFunc that indexes Freight by the // Stages in which it has been verified. func FreightByVerifiedStages(obj client.Object) []string { diff --git a/internal/indexer/indexer_test.go b/internal/indexer/indexer_test.go index c14ab823a..9526b1656 100644 --- a/internal/indexer/indexer_test.go +++ b/internal/indexer/indexer_test.go @@ -443,6 +443,40 @@ func TestFreightByWarehouse(t *testing.T) { } } +func TestFreightByCurrentStages(t *testing.T) { + testCases := []struct { + name string + freight *kargoapi.Freight + expected []string + }{ + { + name: "Freight is not currently in use by any Stages", + freight: &kargoapi.Freight{}, + expected: []string{}, + }, + { + name: "Freight is currently in use by a Stage", + freight: &kargoapi.Freight{ + Status: kargoapi.FreightStatus{ + CurrentlyIn: map[string]kargoapi.CurrentStage{"fake-stage": {}}, + }, + }, + expected: []string{"fake-stage"}, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Run(testCase.name, func(t *testing.T) { + require.Equal( + t, + testCase.expected, + FreightByCurrentStages(testCase.freight), + ) + }) + }) + } +} + func TestFreightByVerifiedStages(t *testing.T) { testCases := []struct { name string diff --git a/internal/webhook/freight/webhook.go b/internal/webhook/freight/webhook.go index d985545b6..d8960bdff 100644 --- a/internal/webhook/freight/webhook.go +++ b/internal/webhook/freight/webhook.go @@ -277,7 +277,7 @@ func (w *webhook) ValidateUpdate( // Record Freight approved events if the request doesn't come from Kargo controlplane. if !w.isRequestFromKargoControlplaneFn(req) { for approvedStage := range newFreight.Status.ApprovedFor { - if _, ok := oldFreight.Status.ApprovedFor[approvedStage]; !ok { + if !oldFreight.IsApprovedFor(approvedStage) { w.recordFreightApprovedEvent(req, newFreight, approvedStage) } } diff --git a/internal/webhook/promotion/webhook.go b/internal/webhook/promotion/webhook.go index b1ca681ac..e0895ce81 100644 --- a/internal/webhook/promotion/webhook.go +++ b/internal/webhook/promotion/webhook.go @@ -246,7 +246,7 @@ func (w *webhook) ValidateCreate( return nil, fmt.Errorf("get freight: %w", err) } - if !kargoapi.IsFreightAvailable(stage, freight) { + if !stage.IsFreightAvailable(freight) { return nil, apierrors.NewInvalid( promotionGroupKind, promo.Name, diff --git a/ui/src/gen/schema/freights.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/freights.kargo.akuity.io_v1alpha1.json index fb8b7ebce..d55e9a73a 100644 --- a/ui/src/gen/schema/freights.kargo.akuity.io_v1alpha1.json +++ b/ui/src/gen/schema/freights.kargo.akuity.io_v1alpha1.json @@ -114,7 +114,7 @@ "type": "string" }, "name": { - "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originated.", + "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originate.", "type": "string" } }, @@ -142,10 +142,29 @@ "description": "ApprovedFor describes the Stages for which this Freight has been approved\npreemptively/manually by a user. This is useful for hotfixes, where one\nmight wish to promote a piece of Freight to a given Stage without\ntransiting the entire pipeline.", "type": "object" }, + "currentlyIn": { + "additionalProperties": { + "description": "CurrentStage reflects a Stage's current use of Freight.", + "properties": { + "since": { + "description": "Since is the time at which the Stage most recently started using the\nFreight. This can be used to calculate how long the Freight has been in use\nby the Stage.", + "format": "date-time", + "type": "string" + } + }, + "type": "object" + }, + "description": "CurrentlyIn describes the Stages in which this Freight is currently in use.", + "type": "object" + }, "verifiedIn": { "additionalProperties": { "description": "VerifiedStage describes a Stage in which Freight has been verified.", "properties": { + "longestSoak": { + "description": "LongestCompletedSoak represents the longest definite time interval wherein\nthe Freight was in CONTINUOUS use by the Stage. This value is updated as\nFreight EXITS the Stage. If the Freight is currently in use by the Stage,\nthe time elapsed since the Freight ENTERED the Stage is its current soak\ntime, which may exceed the value of this field.", + "type": "string" + }, "verifiedAt": { "description": "VerifiedAt is the time at which the Freight was verified in the Stage.", "format": "date-time", diff --git a/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json index c28cf84a2..6b4d0dd12 100644 --- a/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json +++ b/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json @@ -220,7 +220,7 @@ "type": "string" }, "name": { - "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originated.", + "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originate.", "type": "string" } }, @@ -345,7 +345,7 @@ "type": "string" }, "name": { - "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originated.", + "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originate.", "type": "string" } }, diff --git a/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json index c912502af..ebdd3ccb2 100644 --- a/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json +++ b/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json @@ -115,7 +115,7 @@ "type": "string" }, "name": { - "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originated.", + "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originate.", "type": "string" } }, @@ -132,6 +132,11 @@ "description": "Direct indicates the requested Freight may be obtained directly from the\nWarehouse from which it originated. If this field's value is false, then\nthe value of the Stages field must be non-empty. i.e. Between the two\nfields, at least one source must be specified.", "type": "boolean" }, + "requiredSoakTime": { + "description": "RequiredSoakTime specifies a minimum duration for which the requested\nFreight must have continuously occupied (\"soaked in\") in an upstream Stage\nbefore becoming available for promotion to this Stage. This is an optional\nfield. If nil or zero, no soak time is required. Any soak time requirement\nis in ADDITION to the requirement that Freight be verified in an upstream\nStage to become available for promotion to this Stage, although a manual\napproval for promotion to this Stage will supersede any soak time\nrequirement.", + "pattern": "^([0-9]+(\\.[0-9]+)?(s|m|h))+$", + "type": "string" + }, "stages": { "description": "Stages identifies other \"upstream\" Stages as potential sources of the\nrequested Freight. If this field's value is empty, then the value of the\nDirect field must be true. i.e. Between the two fields, at least on source\nmust be specified.", "items": { @@ -402,7 +407,7 @@ "type": "string" }, "name": { - "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originated.", + "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originate.", "type": "string" } }, @@ -538,7 +543,7 @@ "type": "string" }, "name": { - "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originated.", + "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originate.", "type": "string" } }, @@ -663,7 +668,7 @@ "type": "string" }, "name": { - "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originated.", + "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originate.", "type": "string" } }, @@ -944,7 +949,7 @@ "type": "string" }, "name": { - "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originated.", + "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originate.", "type": "string" } }, @@ -1172,7 +1177,7 @@ "type": "string" }, "name": { - "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originated.", + "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originate.", "type": "string" } }, @@ -1308,7 +1313,7 @@ "type": "string" }, "name": { - "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originated.", + "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originate.", "type": "string" } }, @@ -1433,7 +1438,7 @@ "type": "string" }, "name": { - "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originated.", + "description": "Name is the name of the resource of the kind indicated by the Kind field\nfrom which Freight may originate.", "type": "string" } }, diff --git a/ui/src/gen/v1alpha1/generated_pb.ts b/ui/src/gen/v1alpha1/generated_pb.ts index df665a5c3..27ef3419e 100644 --- a/ui/src/gen/v1alpha1/generated_pb.ts +++ b/ui/src/gen/v1alpha1/generated_pb.ts @@ -18,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file v1alpha1/generated.proto. */ export const file_v1alpha1_generated: GenFile = /*@__PURE__*/ - fileDesc("Chh2MWFscGhhMS9nZW5lcmF0ZWQucHJvdG8SJGdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMSIyChNBbmFseXNpc1J1bkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAkisAIKE0FuYWx5c2lzUnVuTWV0YWRhdGESVQoGbGFiZWxzGAEgAygLMkUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuTWV0YWRhdGEuTGFiZWxzRW50cnkSXwoLYW5ub3RhdGlvbnMYAiADKAsySi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQW5hbHlzaXNSdW5NZXRhZGF0YS5Bbm5vdGF0aW9uc0VudHJ5Gi0KC0xhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMgoQQW5ub3RhdGlvbnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIkYKFEFuYWx5c2lzUnVuUmVmZXJlbmNlEhEKCW5hbWVzcGFjZRgBIAEoCRIMCgRuYW1lGAIgASgJEg0KBXBoYXNlGAMgASgJIikKGUFuYWx5c2lzVGVtcGxhdGVSZWZlcmVuY2USDAoEbmFtZRgBIAEoCSJPCg1BcHByb3ZlZFN0YWdlEj4KCmFwcHJvdmVkQXQYASABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSI4ChVBcmdvQ0RBcHBIZWFsdGhTdGF0dXMSDgoGc3RhdHVzGAEgASgJEg8KB21lc3NhZ2UYAiABKAki1AEKD0FyZ29DREFwcFN0YXR1cxIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRJRCgxoZWFsdGhTdGF0dXMYAyABKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwSGVhbHRoU3RhdHVzEk0KCnN5bmNTdGF0dXMYBCABKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwU3luY1N0YXR1cyJKChNBcmdvQ0RBcHBTeW5jU3RhdHVzEg4KBnN0YXR1cxgBIAEoCRIQCghyZXZpc2lvbhgCIAEoCRIRCglyZXZpc2lvbnMYAyADKAkiNwoFQ2hhcnQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiYQoUQ2hhcnREaXNjb3ZlcnlSZXN1bHQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSEAoIdmVyc2lvbnMYBCADKAkiZAoRQ2hhcnRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSFgoOZGlzY292ZXJ5TGltaXQYBCABKAUitgIKE0Rpc2NvdmVyZWRBcnRpZmFjdHMSQAoMZGlzY292ZXJlZEF0GAQgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUSRQoDZ2l0GAEgAygLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkdpdERpc2NvdmVyeVJlc3VsdBJKCgZpbWFnZXMYAiADKAsyOi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuSW1hZ2VEaXNjb3ZlcnlSZXN1bHQSSgoGY2hhcnRzGAMgAygLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkNoYXJ0RGlzY292ZXJ5UmVzdWx0IrABChBEaXNjb3ZlcmVkQ29tbWl0EgoKAmlkGAEgASgJEg4KBmJyYW5jaBgCIAEoCRILCgN0YWcYAyABKAkSDwoHc3ViamVjdBgEIAEoCRIOCgZhdXRob3IYBSABKAkSEQoJY29tbWl0dGVyGAYgASgJEj8KC2NyZWF0b3JEYXRlGAcgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUiigEKGERpc2NvdmVyZWRJbWFnZVJlZmVyZW5jZRILCgN0YWcYASABKAkSDgoGZGlnZXN0GAIgASgJEhIKCmdpdFJlcG9VUkwYAyABKAkSPQoJY3JlYXRlZEF0GAQgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUiogMKB0ZyZWlnaHQSQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRINCgVhbGlhcxgHIAEoCRJDCgZvcmlnaW4YCSABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodE9yaWdpbhJACgdjb21taXRzGAMgAygLMi8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkdpdENvbW1pdBI7CgZpbWFnZXMYBCADKAsyKy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuSW1hZ2USOwoGY2hhcnRzGAUgAygLMisuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkNoYXJ0EkMKBnN0YXR1cxgGIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0U3RhdHVzIq0CChFGcmVpZ2h0Q29sbGVjdGlvbhIKCgJpZBgDIAEoCRJRCgVpdGVtcxgBIAMoCzJCLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0Q29sbGVjdGlvbi5JdGVtc0VudHJ5ElMKE3ZlcmlmaWNhdGlvbkhpc3RvcnkYAiADKAsyNi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuVmVyaWZpY2F0aW9uSW5mbxpkCgpJdGVtc0VudHJ5EgsKA2tleRgBIAEoCRJFCgV2YWx1ZRgCIAEoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0UmVmZXJlbmNlOgI4ASKNAQoLRnJlaWdodExpc3QSQAoIbWV0YWRhdGEYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuTGlzdE1ldGESPAoFaXRlbXMYAiADKAsyLS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodCIrCg1GcmVpZ2h0T3JpZ2luEgwKBGtpbmQYASABKAkSDAoEbmFtZRgCIAEoCSKhAgoQRnJlaWdodFJlZmVyZW5jZRIMCgRuYW1lGAEgASgJEkMKBm9yaWdpbhgIIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0T3JpZ2luEkAKB2NvbW1pdHMYAiADKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuR2l0Q29tbWl0EjsKBmltYWdlcxgDIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5JbWFnZRI7CgZjaGFydHMYBCADKAsyKy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQ2hhcnQinAEKDkZyZWlnaHRSZXF1ZXN0EkMKBm9yaWdpbhgBIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0T3JpZ2luEkUKB3NvdXJjZXMYAiABKAsyNC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodFNvdXJjZXMiMAoORnJlaWdodFNvdXJjZXMSDgoGZGlyZWN0GAEgASgIEg4KBnN0YWdlcxgCIAMoCSKUAwoNRnJlaWdodFN0YXR1cxJXCgp2ZXJpZmllZEluGAEgAygLMkMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuVmVyaWZpZWRJbkVudHJ5ElkKC2FwcHJvdmVkRm9yGAIgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQXBwcm92ZWRGb3JFbnRyeRpmCg9WZXJpZmllZEluRW50cnkSCwoDa2V5GAEgASgJEkIKBXZhbHVlGAIgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlZlcmlmaWVkU3RhZ2U6AjgBGmcKEEFwcHJvdmVkRm9yRW50cnkSCwoDa2V5GAEgASgJEkIKBXZhbHVlGAIgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFwcHJvdmVkU3RhZ2U6AjgBInkKCUdpdENvbW1pdBIPCgdyZXBvVVJMGAEgASgJEgoKAmlkGAIgASgJEg4KBmJyYW5jaBgDIAEoCRILCgN0YWcYBCABKAkSDwoHbWVzc2FnZRgGIAEoCRIOCgZhdXRob3IYByABKAkSEQoJY29tbWl0dGVyGAggASgJIm4KEkdpdERpc2NvdmVyeVJlc3VsdBIPCgdyZXBvVVJMGAEgASgJEkcKB2NvbW1pdHMYAiADKAsyNi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRGlzY292ZXJlZENvbW1pdCKOAgoPR2l0U3Vic2NyaXB0aW9uEg8KB3JlcG9VUkwYASABKAkSHwoXY29tbWl0U2VsZWN0aW9uU3RyYXRlZ3kYAiABKAkSDgoGYnJhbmNoGAMgASgJEhUKDXN0cmljdFNlbXZlcnMYCyABKAgSGAoQc2VtdmVyQ29uc3RyYWludBgEIAEoCRIRCglhbGxvd1RhZ3MYBSABKAkSEgoKaWdub3JlVGFncxgGIAMoCRIdChVpbnNlY3VyZVNraXBUTFNWZXJpZnkYByABKAgSFAoMaW5jbHVkZVBhdGhzGAggAygJEhQKDGV4Y2x1ZGVQYXRocxgJIAMoCRIWCg5kaXNjb3ZlcnlMaW1pdBgKIAEoBSLIAQoGSGVhbHRoEg4KBnN0YXR1cxgBIAEoCRIOCgZpc3N1ZXMYAiADKAkSTgoGY29uZmlnGAQgASgLMj4uazhzLmlvLmFwaWV4dGVuc2lvbnNfYXBpc2VydmVyLnBrZy5hcGlzLmFwaWV4dGVuc2lvbnMudjEuSlNPThJOCgZvdXRwdXQYBSABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OIm8KD0hlYWx0aENoZWNrU3RlcBIMCgR1c2VzGAEgASgJEk4KBmNvbmZpZxgCIAEoCzI+Lms4cy5pby5hcGlleHRlbnNpb25zX2FwaXNlcnZlci5wa2cuYXBpcy5hcGlleHRlbnNpb25zLnYxLkpTT04iSQoFSW1hZ2USDwoHcmVwb1VSTBgBIAEoCRISCgpnaXRSZXBvVVJMGAIgASgJEgsKA3RhZxgDIAEoCRIOCgZkaWdlc3QYBCABKAkijQEKFEltYWdlRGlzY292ZXJ5UmVzdWx0Eg8KB3JlcG9VUkwYASABKAkSEAoIcGxhdGZvcm0YAiABKAkSUgoKcmVmZXJlbmNlcxgDIAMoCzI+LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2Ui+QEKEUltYWdlU3Vic2NyaXB0aW9uEg8KB3JlcG9VUkwYASABKAkSEgoKZ2l0UmVwb1VSTBgCIAEoCRIeChZpbWFnZVNlbGVjdGlvblN0cmF0ZWd5GAMgASgJEhUKDXN0cmljdFNlbXZlcnMYCiABKAgSGAoQc2VtdmVyQ29uc3RyYWludBgEIAEoCRIRCglhbGxvd1RhZ3MYBSABKAkSEgoKaWdub3JlVGFncxgGIAMoCRIQCghwbGF0Zm9ybRgHIAEoCRIdChVpbnNlY3VyZVNraXBUTFNWZXJpZnkYCCABKAgSFgoOZGlzY292ZXJ5TGltaXQYCSABKAUi0wEKB1Byb2plY3QSQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRI/CgRzcGVjGAIgASgLMjEuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb2plY3RTcGVjEkMKBnN0YXR1cxgDIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9qZWN0U3RhdHVzIo0BCgtQcm9qZWN0TGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI8CgVpdGVtcxgCIAMoCzItLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9qZWN0Il8KC1Byb2plY3RTcGVjElAKEXByb21vdGlvblBvbGljaWVzGAEgAygLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblBvbGljeSJ0Cg1Qcm9qZWN0U3RhdHVzEkMKCmNvbmRpdGlvbnMYAyADKAsyLy5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuQ29uZGl0aW9uEg0KBXBoYXNlGAEgASgJEg8KB21lc3NhZ2UYAiABKAki2QEKCVByb21vdGlvbhJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEkEKBHNwZWMYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3BlYxJFCgZzdGF0dXMYAyABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RhdHVzIpEBCg1Qcm9tb3Rpb25MaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEj4KBWl0ZW1zGAIgAygLMi8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvbiI+Cg9Qcm9tb3Rpb25Qb2xpY3kSDQoFc3RhZ2UYASABKAkSHAoUYXV0b1Byb21vdGlvbkVuYWJsZWQYAiABKAgi8gEKElByb21vdGlvblJlZmVyZW5jZRIMCgRuYW1lGAEgASgJEkcKB2ZyZWlnaHQYAiABKAsyNi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodFJlZmVyZW5jZRJFCgZzdGF0dXMYAyABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RhdHVzEj4KCmZpbmlzaGVkQXQYBCABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSK6AQoNUHJvbW90aW9uU3BlYxINCgVzdGFnZRgBIAEoCRIPCgdmcmVpZ2h0GAIgASgJEkUKBHZhcnMYBCADKAsyNy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVmFyaWFibGUSQgoFc3RlcHMYAyADKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RlcCK3BAoPUHJvbW90aW9uU3RhdHVzEhoKEmxhc3RIYW5kbGVkUmVmcmVzaBgEIAEoCRINCgVwaGFzZRgBIAEoCRIPCgdtZXNzYWdlGAIgASgJEkcKB2ZyZWlnaHQYBSABKAsyNi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodFJlZmVyZW5jZRJSChFmcmVpZ2h0Q29sbGVjdGlvbhgHIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0Q29sbGVjdGlvbhJLCgxoZWFsdGhDaGVja3MYCCADKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuSGVhbHRoQ2hlY2tTdGVwEj4KCmZpbmlzaGVkQXQYBiABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRITCgtjdXJyZW50U3RlcBgJIAEoAxJaChVzdGVwRXhlY3V0aW9uTWV0YWRhdGEYCyADKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuU3RlcEV4ZWN1dGlvbk1ldGFkYXRhEk0KBXN0YXRlGAogASgLMj4uazhzLmlvLmFwaWV4dGVuc2lvbnNfYXBpc2VydmVyLnBrZy5hcGlzLmFwaWV4dGVuc2lvbnMudjEuSlNPTiLCAQoNUHJvbW90aW9uU3RlcBIMCgR1c2VzGAEgASgJEgoKAmFzGAIgASgJEkcKBXJldHJ5GAQgASgLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblN0ZXBSZXRyeRJOCgZjb25maWcYAyABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OIm0KElByb21vdGlvblN0ZXBSZXRyeRI/Cgd0aW1lb3V0GAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEhYKDmVycm9yVGhyZXNob2xkGAIgASgNIl4KEVByb21vdGlvblRlbXBsYXRlEkkKBHNwZWMYASABKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGVtcGxhdGVTcGVjIqIBChVQcm9tb3Rpb25UZW1wbGF0ZVNwZWMSRQoEdmFycxgCIAMoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25WYXJpYWJsZRJCCgVzdGVwcxgBIAMoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25TdGVwIjAKEVByb21vdGlvblZhcmlhYmxlEgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAki5gEKEFJlcG9TdWJzY3JpcHRpb24SQgoDZ2l0GAEgASgLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkdpdFN1YnNjcmlwdGlvbhJGCgVpbWFnZRgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5JbWFnZVN1YnNjcmlwdGlvbhJGCgVjaGFydBgDIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydFN1YnNjcmlwdGlvbiLNAQoFU3RhZ2USQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRI9CgRzcGVjGAIgASgLMi8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlN0YWdlU3BlYxJBCgZzdGF0dXMYAyABKAsyMS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuU3RhZ2VTdGF0dXMiiQEKCVN0YWdlTGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI6CgVpdGVtcxgCIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5TdGFnZSKIAgoJU3RhZ2VTcGVjEg0KBXNoYXJkGAQgASgJEk4KEHJlcXVlc3RlZEZyZWlnaHQYBSADKAsyNC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodFJlcXVlc3QSUgoRcHJvbW90aW9uVGVtcGxhdGUYBiABKAsyNy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGVtcGxhdGUSSAoMdmVyaWZpY2F0aW9uGAMgASgLMjIuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlZlcmlmaWNhdGlvbiL2AwoLU3RhZ2VTdGF0dXMSQwoKY29uZGl0aW9ucxgNIAMoCzIvLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5Db25kaXRpb24SGgoSbGFzdEhhbmRsZWRSZWZyZXNoGAsgASgJEg0KBXBoYXNlGAEgASgJEk8KDmZyZWlnaHRIaXN0b3J5GAQgAygLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRDb2xsZWN0aW9uEhYKDmZyZWlnaHRTdW1tYXJ5GAwgASgJEjwKBmhlYWx0aBgIIAEoCzIsLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5IZWFsdGgSDwoHbWVzc2FnZRgJIAEoCRIaChJvYnNlcnZlZEdlbmVyYXRpb24YBiABKAMSUgoQY3VycmVudFByb21vdGlvbhgHIAEoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25SZWZlcmVuY2USTwoNbGFzdFByb21vdGlvbhgKIAEoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25SZWZlcmVuY2Ui2gEKFVN0ZXBFeGVjdXRpb25NZXRhZGF0YRINCgVhbGlhcxgBIAEoCRI9CglzdGFydGVkQXQYAiABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRI+CgpmaW5pc2hlZEF0GAMgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUSEgoKZXJyb3JDb3VudBgEIAEoDRIOCgZzdGF0dXMYBSABKAkSDwoHbWVzc2FnZRgGIAEoCSKLAgoMVmVyaWZpY2F0aW9uEloKEWFuYWx5c2lzVGVtcGxhdGVzGAEgAygLMj8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzVGVtcGxhdGVSZWZlcmVuY2USVgoTYW5hbHlzaXNSdW5NZXRhZGF0YRgCIAEoCzI5LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5BbmFseXNpc1J1bk1ldGFkYXRhEkcKBGFyZ3MYAyADKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQW5hbHlzaXNSdW5Bcmd1bWVudCKdAgoQVmVyaWZpY2F0aW9uSW5mbxIKCgJpZBgEIAEoCRINCgVhY3RvchgHIAEoCRI9CglzdGFydFRpbWUYBSABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRINCgVwaGFzZRgBIAEoCRIPCgdtZXNzYWdlGAIgASgJEk8KC2FuYWx5c2lzUnVuGAMgASgLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuUmVmZXJlbmNlEj4KCmZpbmlzaFRpbWUYBiABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSJPCg1WZXJpZmllZFN0YWdlEj4KCnZlcmlmaWVkQXQYASABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSLZAQoJV2FyZWhvdXNlEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESQQoEc3BlYxgCIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5XYXJlaG91c2VTcGVjEkUKBnN0YXR1cxgDIAEoCzI1LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5XYXJlaG91c2VTdGF0dXMikQEKDVdhcmVob3VzZUxpc3QSQAoIbWV0YWRhdGEYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuTGlzdE1ldGESPgoFaXRlbXMYAiADKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuV2FyZWhvdXNlIs4BCg1XYXJlaG91c2VTcGVjEg0KBXNoYXJkGAIgASgJEkAKCGludGVydmFsGAQgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEh0KFWZyZWlnaHRDcmVhdGlvblBvbGljeRgDIAEoCRJNCg1zdWJzY3JpcHRpb25zGAEgAygLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlJlcG9TdWJzY3JpcHRpb24i/QEKD1dhcmVob3VzZVN0YXR1cxJDCgpjb25kaXRpb25zGAkgAygLMi8uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkNvbmRpdGlvbhIaChJsYXN0SGFuZGxlZFJlZnJlc2gYBiABKAkSGgoSb2JzZXJ2ZWRHZW5lcmF0aW9uGAQgASgDEhUKDWxhc3RGcmVpZ2h0SUQYCCABKAkSVgoTZGlzY292ZXJlZEFydGlmYWN0cxgHIAEoCzI5LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkQXJ0aWZhY3RzQpcCCihjb20uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExQg5HZW5lcmF0ZWRQcm90b1ABWiRnaXRodWIuY29tL2FrdWl0eS9rYXJnby9hcGkvdjFhbHBoYTGiAgVHQ0FLQaoCJEdpdGh1Yi5Db20uQWt1aXR5LkthcmdvLkFwaS5WMWFscGhhMcoCJEdpdGh1YlxDb21cQWt1aXR5XEthcmdvXEFwaVxWMWFscGhhMeICMEdpdGh1YlxDb21cQWt1aXR5XEthcmdvXEFwaVxWMWFscGhhMVxHUEJNZXRhZGF0YeoCKUdpdGh1Yjo6Q29tOjpBa3VpdHk6OkthcmdvOjpBcGk6OlYxYWxwaGEx", [file_k8s_io_apiextensions_apiserver_pkg_apis_apiextensions_v1_generated, file_k8s_io_apimachinery_pkg_apis_meta_v1_generated, file_k8s_io_apimachinery_pkg_runtime_generated, file_k8s_io_apimachinery_pkg_runtime_schema_generated]); + fileDesc("Chh2MWFscGhhMS9nZW5lcmF0ZWQucHJvdG8SJGdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMSIyChNBbmFseXNpc1J1bkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAkisAIKE0FuYWx5c2lzUnVuTWV0YWRhdGESVQoGbGFiZWxzGAEgAygLMkUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuTWV0YWRhdGEuTGFiZWxzRW50cnkSXwoLYW5ub3RhdGlvbnMYAiADKAsySi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQW5hbHlzaXNSdW5NZXRhZGF0YS5Bbm5vdGF0aW9uc0VudHJ5Gi0KC0xhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMgoQQW5ub3RhdGlvbnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIkYKFEFuYWx5c2lzUnVuUmVmZXJlbmNlEhEKCW5hbWVzcGFjZRgBIAEoCRIMCgRuYW1lGAIgASgJEg0KBXBoYXNlGAMgASgJIikKGUFuYWx5c2lzVGVtcGxhdGVSZWZlcmVuY2USDAoEbmFtZRgBIAEoCSJPCg1BcHByb3ZlZFN0YWdlEj4KCmFwcHJvdmVkQXQYASABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSI4ChVBcmdvQ0RBcHBIZWFsdGhTdGF0dXMSDgoGc3RhdHVzGAEgASgJEg8KB21lc3NhZ2UYAiABKAki1AEKD0FyZ29DREFwcFN0YXR1cxIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRJRCgxoZWFsdGhTdGF0dXMYAyABKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwSGVhbHRoU3RhdHVzEk0KCnN5bmNTdGF0dXMYBCABKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwU3luY1N0YXR1cyJKChNBcmdvQ0RBcHBTeW5jU3RhdHVzEg4KBnN0YXR1cxgBIAEoCRIQCghyZXZpc2lvbhgCIAEoCRIRCglyZXZpc2lvbnMYAyADKAkiNwoFQ2hhcnQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiYQoUQ2hhcnREaXNjb3ZlcnlSZXN1bHQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSEAoIdmVyc2lvbnMYBCADKAkiZAoRQ2hhcnRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSFgoOZGlzY292ZXJ5TGltaXQYBCABKAUiSQoMQ3VycmVudFN0YWdlEjkKBXNpbmNlGAEgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUitgIKE0Rpc2NvdmVyZWRBcnRpZmFjdHMSQAoMZGlzY292ZXJlZEF0GAQgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUSRQoDZ2l0GAEgAygLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkdpdERpc2NvdmVyeVJlc3VsdBJKCgZpbWFnZXMYAiADKAsyOi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuSW1hZ2VEaXNjb3ZlcnlSZXN1bHQSSgoGY2hhcnRzGAMgAygLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkNoYXJ0RGlzY292ZXJ5UmVzdWx0IrABChBEaXNjb3ZlcmVkQ29tbWl0EgoKAmlkGAEgASgJEg4KBmJyYW5jaBgCIAEoCRILCgN0YWcYAyABKAkSDwoHc3ViamVjdBgEIAEoCRIOCgZhdXRob3IYBSABKAkSEQoJY29tbWl0dGVyGAYgASgJEj8KC2NyZWF0b3JEYXRlGAcgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUiigEKGERpc2NvdmVyZWRJbWFnZVJlZmVyZW5jZRILCgN0YWcYASABKAkSDgoGZGlnZXN0GAIgASgJEhIKCmdpdFJlcG9VUkwYAyABKAkSPQoJY3JlYXRlZEF0GAQgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUiogMKB0ZyZWlnaHQSQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRINCgVhbGlhcxgHIAEoCRJDCgZvcmlnaW4YCSABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodE9yaWdpbhJACgdjb21taXRzGAMgAygLMi8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkdpdENvbW1pdBI7CgZpbWFnZXMYBCADKAsyKy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuSW1hZ2USOwoGY2hhcnRzGAUgAygLMisuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkNoYXJ0EkMKBnN0YXR1cxgGIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0U3RhdHVzIq0CChFGcmVpZ2h0Q29sbGVjdGlvbhIKCgJpZBgDIAEoCRJRCgVpdGVtcxgBIAMoCzJCLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0Q29sbGVjdGlvbi5JdGVtc0VudHJ5ElMKE3ZlcmlmaWNhdGlvbkhpc3RvcnkYAiADKAsyNi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuVmVyaWZpY2F0aW9uSW5mbxpkCgpJdGVtc0VudHJ5EgsKA2tleRgBIAEoCRJFCgV2YWx1ZRgCIAEoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0UmVmZXJlbmNlOgI4ASKNAQoLRnJlaWdodExpc3QSQAoIbWV0YWRhdGEYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuTGlzdE1ldGESPAoFaXRlbXMYAiADKAsyLS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodCIrCg1GcmVpZ2h0T3JpZ2luEgwKBGtpbmQYASABKAkSDAoEbmFtZRgCIAEoCSKhAgoQRnJlaWdodFJlZmVyZW5jZRIMCgRuYW1lGAEgASgJEkMKBm9yaWdpbhgIIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0T3JpZ2luEkAKB2NvbW1pdHMYAiADKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuR2l0Q29tbWl0EjsKBmltYWdlcxgDIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5JbWFnZRI7CgZjaGFydHMYBCADKAsyKy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQ2hhcnQinAEKDkZyZWlnaHRSZXF1ZXN0EkMKBm9yaWdpbhgBIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0T3JpZ2luEkUKB3NvdXJjZXMYAiABKAsyNC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodFNvdXJjZXMiegoORnJlaWdodFNvdXJjZXMSDgoGZGlyZWN0GAEgASgIEg4KBnN0YWdlcxgCIAMoCRJIChByZXF1aXJlZFNvYWtUaW1lGAMgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uItcECg1GcmVpZ2h0U3RhdHVzElkKC2N1cnJlbnRseUluGAMgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQ3VycmVudGx5SW5FbnRyeRJXCgp2ZXJpZmllZEluGAEgAygLMkMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuVmVyaWZpZWRJbkVudHJ5ElkKC2FwcHJvdmVkRm9yGAIgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQXBwcm92ZWRGb3JFbnRyeRpmChBDdXJyZW50bHlJbkVudHJ5EgsKA2tleRgBIAEoCRJBCgV2YWx1ZRgCIAEoCzIyLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DdXJyZW50U3RhZ2U6AjgBGmYKD1ZlcmlmaWVkSW5FbnRyeRILCgNrZXkYASABKAkSQgoFdmFsdWUYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuVmVyaWZpZWRTdGFnZToCOAEaZwoQQXBwcm92ZWRGb3JFbnRyeRILCgNrZXkYASABKAkSQgoFdmFsdWUYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXBwcm92ZWRTdGFnZToCOAEieQoJR2l0Q29tbWl0Eg8KB3JlcG9VUkwYASABKAkSCgoCaWQYAiABKAkSDgoGYnJhbmNoGAMgASgJEgsKA3RhZxgEIAEoCRIPCgdtZXNzYWdlGAYgASgJEg4KBmF1dGhvchgHIAEoCRIRCgljb21taXR0ZXIYCCABKAkibgoSR2l0RGlzY292ZXJ5UmVzdWx0Eg8KB3JlcG9VUkwYASABKAkSRwoHY29tbWl0cxgCIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkQ29tbWl0Io4CCg9HaXRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIfChdjb21taXRTZWxlY3Rpb25TdHJhdGVneRgCIAEoCRIOCgZicmFuY2gYAyABKAkSFQoNc3RyaWN0U2VtdmVycxgLIAEoCBIYChBzZW12ZXJDb25zdHJhaW50GAQgASgJEhEKCWFsbG93VGFncxgFIAEoCRISCgppZ25vcmVUYWdzGAYgAygJEh0KFWluc2VjdXJlU2tpcFRMU1ZlcmlmeRgHIAEoCBIUCgxpbmNsdWRlUGF0aHMYCCADKAkSFAoMZXhjbHVkZVBhdGhzGAkgAygJEhYKDmRpc2NvdmVyeUxpbWl0GAogASgFIsgBCgZIZWFsdGgSDgoGc3RhdHVzGAEgASgJEg4KBmlzc3VlcxgCIAMoCRJOCgZjb25maWcYBCABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OEk4KBm91dHB1dBgFIAEoCzI+Lms4cy5pby5hcGlleHRlbnNpb25zX2FwaXNlcnZlci5wa2cuYXBpcy5hcGlleHRlbnNpb25zLnYxLkpTT04ibwoPSGVhbHRoQ2hlY2tTdGVwEgwKBHVzZXMYASABKAkSTgoGY29uZmlnGAIgASgLMj4uazhzLmlvLmFwaWV4dGVuc2lvbnNfYXBpc2VydmVyLnBrZy5hcGlzLmFwaWV4dGVuc2lvbnMudjEuSlNPTiJJCgVJbWFnZRIPCgdyZXBvVVJMGAEgASgJEhIKCmdpdFJlcG9VUkwYAiABKAkSCwoDdGFnGAMgASgJEg4KBmRpZ2VzdBgEIAEoCSKNAQoUSW1hZ2VEaXNjb3ZlcnlSZXN1bHQSDwoHcmVwb1VSTBgBIAEoCRIQCghwbGF0Zm9ybRgCIAEoCRJSCgpyZWZlcmVuY2VzGAMgAygLMj4uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkRpc2NvdmVyZWRJbWFnZVJlZmVyZW5jZSL5AQoRSW1hZ2VTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRISCgpnaXRSZXBvVVJMGAIgASgJEh4KFmltYWdlU2VsZWN0aW9uU3RyYXRlZ3kYAyABKAkSFQoNc3RyaWN0U2VtdmVycxgKIAEoCBIYChBzZW12ZXJDb25zdHJhaW50GAQgASgJEhEKCWFsbG93VGFncxgFIAEoCRISCgppZ25vcmVUYWdzGAYgAygJEhAKCHBsYXRmb3JtGAcgASgJEh0KFWluc2VjdXJlU2tpcFRMU1ZlcmlmeRgIIAEoCBIWCg5kaXNjb3ZlcnlMaW1pdBgJIAEoBSLTAQoHUHJvamVjdBJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEj8KBHNwZWMYAiABKAsyMS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvamVjdFNwZWMSQwoGc3RhdHVzGAMgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb2plY3RTdGF0dXMijQEKC1Byb2plY3RMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEjwKBWl0ZW1zGAIgAygLMi0uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb2plY3QiXwoLUHJvamVjdFNwZWMSUAoRcHJvbW90aW9uUG9saWNpZXMYASADKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uUG9saWN5InQKDVByb2plY3RTdGF0dXMSQwoKY29uZGl0aW9ucxgDIAMoCzIvLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5Db25kaXRpb24SDQoFcGhhc2UYASABKAkSDwoHbWVzc2FnZRgCIAEoCSLZAQoJUHJvbW90aW9uEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESQQoEc3BlYxgCIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25TcGVjEkUKBnN0YXR1cxgDIAEoCzI1LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25TdGF0dXMikQEKDVByb21vdGlvbkxpc3QSQAoIbWV0YWRhdGEYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuTGlzdE1ldGESPgoFaXRlbXMYAiADKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uIj4KD1Byb21vdGlvblBvbGljeRINCgVzdGFnZRgBIAEoCRIcChRhdXRvUHJvbW90aW9uRW5hYmxlZBgCIAEoCCLyAQoSUHJvbW90aW9uUmVmZXJlbmNlEgwKBG5hbWUYASABKAkSRwoHZnJlaWdodBgCIAEoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0UmVmZXJlbmNlEkUKBnN0YXR1cxgDIAEoCzI1LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25TdGF0dXMSPgoKZmluaXNoZWRBdBgEIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIroBCg1Qcm9tb3Rpb25TcGVjEg0KBXN0YWdlGAEgASgJEg8KB2ZyZWlnaHQYAiABKAkSRQoEdmFycxgEIAMoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25WYXJpYWJsZRJCCgVzdGVwcxgDIAMoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25TdGVwIrcECg9Qcm9tb3Rpb25TdGF0dXMSGgoSbGFzdEhhbmRsZWRSZWZyZXNoGAQgASgJEg0KBXBoYXNlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSRwoHZnJlaWdodBgFIAEoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0UmVmZXJlbmNlElIKEWZyZWlnaHRDb2xsZWN0aW9uGAcgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRDb2xsZWN0aW9uEksKDGhlYWx0aENoZWNrcxgIIAMoCzI1LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5IZWFsdGhDaGVja1N0ZXASPgoKZmluaXNoZWRBdBgGIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEhMKC2N1cnJlbnRTdGVwGAkgASgDEloKFXN0ZXBFeGVjdXRpb25NZXRhZGF0YRgLIAMoCzI7LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5TdGVwRXhlY3V0aW9uTWV0YWRhdGESTQoFc3RhdGUYCiABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OIsIBCg1Qcm9tb3Rpb25TdGVwEgwKBHVzZXMYASABKAkSCgoCYXMYAiABKAkSRwoFcmV0cnkYBCABKAsyOC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RlcFJldHJ5Ek4KBmNvbmZpZxgDIAEoCzI+Lms4cy5pby5hcGlleHRlbnNpb25zX2FwaXNlcnZlci5wa2cuYXBpcy5hcGlleHRlbnNpb25zLnYxLkpTT04ibQoSUHJvbW90aW9uU3RlcFJldHJ5Ej8KB3RpbWVvdXQYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuRHVyYXRpb24SFgoOZXJyb3JUaHJlc2hvbGQYAiABKA0iXgoRUHJvbW90aW9uVGVtcGxhdGUSSQoEc3BlYxgBIAEoCzI7LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UZW1wbGF0ZVNwZWMiogEKFVByb21vdGlvblRlbXBsYXRlU3BlYxJFCgR2YXJzGAIgAygLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblZhcmlhYmxlEkIKBXN0ZXBzGAEgAygLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblN0ZXAiMAoRUHJvbW90aW9uVmFyaWFibGUSDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSLmAQoQUmVwb1N1YnNjcmlwdGlvbhJCCgNnaXQYASABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuR2l0U3Vic2NyaXB0aW9uEkYKBWltYWdlGAIgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlU3Vic2NyaXB0aW9uEkYKBWNoYXJ0GAMgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkNoYXJ0U3Vic2NyaXB0aW9uIs0BCgVTdGFnZRJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEj0KBHNwZWMYAiABKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuU3RhZ2VTcGVjEkEKBnN0YXR1cxgDIAEoCzIxLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5TdGFnZVN0YXR1cyKJAQoJU3RhZ2VMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEjoKBWl0ZW1zGAIgAygLMisuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlN0YWdlIogCCglTdGFnZVNwZWMSDQoFc2hhcmQYBCABKAkSTgoQcmVxdWVzdGVkRnJlaWdodBgFIAMoCzI0LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0UmVxdWVzdBJSChFwcm9tb3Rpb25UZW1wbGF0ZRgGIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UZW1wbGF0ZRJICgx2ZXJpZmljYXRpb24YAyABKAsyMi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuVmVyaWZpY2F0aW9uIvYDCgtTdGFnZVN0YXR1cxJDCgpjb25kaXRpb25zGA0gAygLMi8uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkNvbmRpdGlvbhIaChJsYXN0SGFuZGxlZFJlZnJlc2gYCyABKAkSDQoFcGhhc2UYASABKAkSTwoOZnJlaWdodEhpc3RvcnkYBCADKAsyNy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodENvbGxlY3Rpb24SFgoOZnJlaWdodFN1bW1hcnkYDCABKAkSPAoGaGVhbHRoGAggASgLMiwuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkhlYWx0aBIPCgdtZXNzYWdlGAkgASgJEhoKEm9ic2VydmVkR2VuZXJhdGlvbhgGIAEoAxJSChBjdXJyZW50UHJvbW90aW9uGAcgASgLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblJlZmVyZW5jZRJPCg1sYXN0UHJvbW90aW9uGAogASgLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblJlZmVyZW5jZSLaAQoVU3RlcEV4ZWN1dGlvbk1ldGFkYXRhEg0KBWFsaWFzGAEgASgJEj0KCXN0YXJ0ZWRBdBgCIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEj4KCmZpbmlzaGVkQXQYAyABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRISCgplcnJvckNvdW50GAQgASgNEg4KBnN0YXR1cxgFIAEoCRIPCgdtZXNzYWdlGAYgASgJIosCCgxWZXJpZmljYXRpb24SWgoRYW5hbHlzaXNUZW1wbGF0ZXMYASADKAsyPy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQW5hbHlzaXNUZW1wbGF0ZVJlZmVyZW5jZRJWChNhbmFseXNpc1J1bk1ldGFkYXRhGAIgASgLMjkuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuTWV0YWRhdGESRwoEYXJncxgDIAMoCzI5LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5BbmFseXNpc1J1bkFyZ3VtZW50Ip0CChBWZXJpZmljYXRpb25JbmZvEgoKAmlkGAQgASgJEg0KBWFjdG9yGAcgASgJEj0KCXN0YXJ0VGltZRgFIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEg0KBXBoYXNlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSTwoLYW5hbHlzaXNSdW4YAyABKAsyOi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQW5hbHlzaXNSdW5SZWZlcmVuY2USPgoKZmluaXNoVGltZRgGIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIpQBCg1WZXJpZmllZFN0YWdlEj4KCnZlcmlmaWVkQXQYASABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRJDCgtsb25nZXN0U29haxgCIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5EdXJhdGlvbiLZAQoJV2FyZWhvdXNlEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESQQoEc3BlYxgCIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5XYXJlaG91c2VTcGVjEkUKBnN0YXR1cxgDIAEoCzI1LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5XYXJlaG91c2VTdGF0dXMikQEKDVdhcmVob3VzZUxpc3QSQAoIbWV0YWRhdGEYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuTGlzdE1ldGESPgoFaXRlbXMYAiADKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuV2FyZWhvdXNlIs4BCg1XYXJlaG91c2VTcGVjEg0KBXNoYXJkGAIgASgJEkAKCGludGVydmFsGAQgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEh0KFWZyZWlnaHRDcmVhdGlvblBvbGljeRgDIAEoCRJNCg1zdWJzY3JpcHRpb25zGAEgAygLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlJlcG9TdWJzY3JpcHRpb24i/QEKD1dhcmVob3VzZVN0YXR1cxJDCgpjb25kaXRpb25zGAkgAygLMi8uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkNvbmRpdGlvbhIaChJsYXN0SGFuZGxlZFJlZnJlc2gYBiABKAkSGgoSb2JzZXJ2ZWRHZW5lcmF0aW9uGAQgASgDEhUKDWxhc3RGcmVpZ2h0SUQYCCABKAkSVgoTZGlzY292ZXJlZEFydGlmYWN0cxgHIAEoCzI5LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkQXJ0aWZhY3RzQpcCCihjb20uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExQg5HZW5lcmF0ZWRQcm90b1ABWiRnaXRodWIuY29tL2FrdWl0eS9rYXJnby9hcGkvdjFhbHBoYTGiAgVHQ0FLQaoCJEdpdGh1Yi5Db20uQWt1aXR5LkthcmdvLkFwaS5WMWFscGhhMcoCJEdpdGh1YlxDb21cQWt1aXR5XEthcmdvXEFwaVxWMWFscGhhMeICMEdpdGh1YlxDb21cQWt1aXR5XEthcmdvXEFwaVxWMWFscGhhMVxHUEJNZXRhZGF0YeoCKUdpdGh1Yjo6Q29tOjpBa3VpdHk6OkthcmdvOjpBcGk6OlYxYWxwaGEx", [file_k8s_io_apiextensions_apiserver_pkg_apis_apiextensions_v1_generated, file_k8s_io_apimachinery_pkg_apis_meta_v1_generated, file_k8s_io_apimachinery_pkg_runtime_generated, file_k8s_io_apimachinery_pkg_runtime_schema_generated]); /** * AnalysisRunArgument represents an argument to be added to an AnalysisRun. @@ -420,6 +420,29 @@ export type ChartSubscription = Message<"github.com.akuity.kargo.api.v1alpha1.Ch export const ChartSubscriptionSchema: GenMessage = /*@__PURE__*/ messageDesc(file_v1alpha1_generated, 10); +/** + * CurrentStage reflects a Stage's current use of Freight. + * + * @generated from message github.com.akuity.kargo.api.v1alpha1.CurrentStage + */ +export type CurrentStage = Message<"github.com.akuity.kargo.api.v1alpha1.CurrentStage"> & { + /** + * Since is the time at which the Stage most recently started using the + * Freight. This can be used to calculate how long the Freight has been in use + * by the Stage. + * + * @generated from field: optional k8s.io.apimachinery.pkg.apis.meta.v1.Time since = 1; + */ + since?: Time; +}; + +/** + * Describes the message github.com.akuity.kargo.api.v1alpha1.CurrentStage. + * Use `create(CurrentStageSchema)` to create a new message. + */ +export const CurrentStageSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1alpha1_generated, 11); + /** * DiscoveredArtifacts holds the artifacts discovered by the Warehouse for its * subscriptions. @@ -472,7 +495,7 @@ export type DiscoveredArtifacts = Message<"github.com.akuity.kargo.api.v1alpha1. * Use `create(DiscoveredArtifactsSchema)` to create a new message. */ export const DiscoveredArtifactsSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 11); + messageDesc(file_v1alpha1_generated, 12); /** * DiscoveredCommit represents a commit discovered by a Warehouse for a @@ -543,7 +566,7 @@ export type DiscoveredCommit = Message<"github.com.akuity.kargo.api.v1alpha1.Dis * Use `create(DiscoveredCommitSchema)` to create a new message. */ export const DiscoveredCommitSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 12); + messageDesc(file_v1alpha1_generated, 13); /** * DiscoveredImageReference represents an image reference discovered by a @@ -596,7 +619,7 @@ export type DiscoveredImageReference = Message<"github.com.akuity.kargo.api.v1al * Use `create(DiscoveredImageReferenceSchema)` to create a new message. */ export const DiscoveredImageReferenceSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 13); + messageDesc(file_v1alpha1_generated, 14); /** * Freight represents a collection of versioned artifacts. @@ -667,7 +690,7 @@ export type Freight = Message<"github.com.akuity.kargo.api.v1alpha1.Freight"> & * Use `create(FreightSchema)` to create a new message. */ export const FreightSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 14); + messageDesc(file_v1alpha1_generated, 15); /** * FreightCollection is a collection of FreightReferences, each of which @@ -707,7 +730,7 @@ export type FreightCollection = Message<"github.com.akuity.kargo.api.v1alpha1.Fr * Use `create(FreightCollectionSchema)` to create a new message. */ export const FreightCollectionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 15); + messageDesc(file_v1alpha1_generated, 16); /** * FreightList is a list of Freight resources. @@ -731,7 +754,7 @@ export type FreightList = Message<"github.com.akuity.kargo.api.v1alpha1.FreightL * Use `create(FreightListSchema)` to create a new message. */ export const FreightListSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 16); + messageDesc(file_v1alpha1_generated, 17); /** * FreightOrigin describes a kind of Freight in terms of where it may have @@ -754,7 +777,7 @@ export type FreightOrigin = Message<"github.com.akuity.kargo.api.v1alpha1.Freigh /** * Name is the name of the resource of the kind indicated by the Kind field - * from which Freight may originated. + * from which Freight may originate. * * +kubebuilder:validation:Required * @@ -768,7 +791,7 @@ export type FreightOrigin = Message<"github.com.akuity.kargo.api.v1alpha1.Freigh * Use `create(FreightOriginSchema)` to create a new message. */ export const FreightOriginSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 17); + messageDesc(file_v1alpha1_generated, 18); /** * FreightReference is a simplified representation of a piece of Freight -- not @@ -820,7 +843,7 @@ export type FreightReference = Message<"github.com.akuity.kargo.api.v1alpha1.Fre * Use `create(FreightReferenceSchema)` to create a new message. */ export const FreightReferenceSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 18); + messageDesc(file_v1alpha1_generated, 19); /** * FreightRequest expresses a Stage's need for Freight having originated from a @@ -853,7 +876,7 @@ export type FreightRequest = Message<"github.com.akuity.kargo.api.v1alpha1.Freig * Use `create(FreightRequestSchema)` to create a new message. */ export const FreightRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 19); + messageDesc(file_v1alpha1_generated, 20); /** * @generated from message github.com.akuity.kargo.api.v1alpha1.FreightSources @@ -878,6 +901,23 @@ export type FreightSources = Message<"github.com.akuity.kargo.api.v1alpha1.Freig * @generated from field: repeated string stages = 2; */ stages: string[]; + + /** + * RequiredSoakTime specifies a minimum duration for which the requested + * Freight must have continuously occupied ("soaked in") in an upstream Stage + * before becoming available for promotion to this Stage. This is an optional + * field. If nil or zero, no soak time is required. Any soak time requirement + * is in ADDITION to the requirement that Freight be verified in an upstream + * Stage to become available for promotion to this Stage, although a manual + * approval for promotion to this Stage will supersede any soak time + * requirement. + * + * +kubebuilder:validation:Type=string + * +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(s|m|h))+$" + * + * @generated from field: optional k8s.io.apimachinery.pkg.apis.meta.v1.Duration requiredSoakTime = 3; + */ + requiredSoakTime?: Duration; }; /** @@ -885,7 +925,7 @@ export type FreightSources = Message<"github.com.akuity.kargo.api.v1alpha1.Freig * Use `create(FreightSourcesSchema)` to create a new message. */ export const FreightSourcesSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 20); + messageDesc(file_v1alpha1_generated, 21); /** * FreightStatus describes a piece of Freight's most recently observed state. @@ -893,6 +933,13 @@ export const FreightSourcesSchema: GenMessage = /*@__PURE__*/ * @generated from message github.com.akuity.kargo.api.v1alpha1.FreightStatus */ export type FreightStatus = Message<"github.com.akuity.kargo.api.v1alpha1.FreightStatus"> & { + /** + * CurrentlyIn describes the Stages in which this Freight is currently in use. + * + * @generated from field: map currentlyIn = 3; + */ + currentlyIn: { [key: string]: CurrentStage }; + /** * VerifiedIn describes the Stages in which this Freight has been verified * through promotion and subsequent health checks. @@ -917,7 +964,7 @@ export type FreightStatus = Message<"github.com.akuity.kargo.api.v1alpha1.Freigh * Use `create(FreightStatusSchema)` to create a new message. */ export const FreightStatusSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 21); + messageDesc(file_v1alpha1_generated, 22); /** * GitCommit describes a specific commit from a specific Git repository. @@ -983,7 +1030,7 @@ export type GitCommit = Message<"github.com.akuity.kargo.api.v1alpha1.GitCommit" * Use `create(GitCommitSchema)` to create a new message. */ export const GitCommitSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 22); + messageDesc(file_v1alpha1_generated, 23); /** * GitDiscoveryResult represents the result of a Git discovery operation for a @@ -1019,7 +1066,7 @@ export type GitDiscoveryResult = Message<"github.com.akuity.kargo.api.v1alpha1.G * Use `create(GitDiscoveryResultSchema)` to create a new message. */ export const GitDiscoveryResultSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 23); + messageDesc(file_v1alpha1_generated, 24); /** * GitSubscription defines a subscription to a Git repository. @@ -1189,7 +1236,7 @@ export type GitSubscription = Message<"github.com.akuity.kargo.api.v1alpha1.GitS * Use `create(GitSubscriptionSchema)` to create a new message. */ export const GitSubscriptionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 24); + messageDesc(file_v1alpha1_generated, 25); /** * Health describes the health of a Stage. @@ -1233,7 +1280,7 @@ export type Health = Message<"github.com.akuity.kargo.api.v1alpha1.Health"> & { * Use `create(HealthSchema)` to create a new message. */ export const HealthSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 25); + messageDesc(file_v1alpha1_generated, 26); /** * HealthCheckStep describes a health check directive which can be executed by @@ -1264,7 +1311,7 @@ export type HealthCheckStep = Message<"github.com.akuity.kargo.api.v1alpha1.Heal * Use `create(HealthCheckStepSchema)` to create a new message. */ export const HealthCheckStepSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 26); + messageDesc(file_v1alpha1_generated, 27); /** * Image describes a specific version of a container image. @@ -1310,7 +1357,7 @@ export type Image = Message<"github.com.akuity.kargo.api.v1alpha1.Image"> & { * Use `create(ImageSchema)` to create a new message. */ export const ImageSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 27); + messageDesc(file_v1alpha1_generated, 28); /** * ImageDiscoveryResult represents the result of an image discovery operation @@ -1356,7 +1403,7 @@ export type ImageDiscoveryResult = Message<"github.com.akuity.kargo.api.v1alpha1 * Use `create(ImageDiscoveryResultSchema)` to create a new message. */ export const ImageDiscoveryResultSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 28); + messageDesc(file_v1alpha1_generated, 29); /** * ImageSubscription defines a subscription to an image repository. @@ -1503,7 +1550,7 @@ export type ImageSubscription = Message<"github.com.akuity.kargo.api.v1alpha1.Im * Use `create(ImageSubscriptionSchema)` to create a new message. */ export const ImageSubscriptionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 29); + messageDesc(file_v1alpha1_generated, 30); /** * Project is a resource type that reconciles to a specially labeled namespace @@ -1537,7 +1584,7 @@ export type Project = Message<"github.com.akuity.kargo.api.v1alpha1.Project"> & * Use `create(ProjectSchema)` to create a new message. */ export const ProjectSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 30); + messageDesc(file_v1alpha1_generated, 31); /** * ProjectList is a list of Project resources. @@ -1561,7 +1608,7 @@ export type ProjectList = Message<"github.com.akuity.kargo.api.v1alpha1.ProjectL * Use `create(ProjectListSchema)` to create a new message. */ export const ProjectListSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 31); + messageDesc(file_v1alpha1_generated, 32); /** * ProjectSpec describes a Project. @@ -1583,7 +1630,7 @@ export type ProjectSpec = Message<"github.com.akuity.kargo.api.v1alpha1.ProjectS * Use `create(ProjectSpecSchema)` to create a new message. */ export const ProjectSpecSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 32); + messageDesc(file_v1alpha1_generated, 33); /** * ProjectStatus describes a Project's current status. @@ -1629,7 +1676,7 @@ export type ProjectStatus = Message<"github.com.akuity.kargo.api.v1alpha1.Projec * Use `create(ProjectStatusSchema)` to create a new message. */ export const ProjectStatusSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 33); + messageDesc(file_v1alpha1_generated, 34); /** * Promotion represents a request to transition a particular Stage into a @@ -1667,7 +1714,7 @@ export type Promotion = Message<"github.com.akuity.kargo.api.v1alpha1.Promotion" * Use `create(PromotionSchema)` to create a new message. */ export const PromotionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 34); + messageDesc(file_v1alpha1_generated, 35); /** * PromotionList contains a list of Promotion @@ -1691,7 +1738,7 @@ export type PromotionList = Message<"github.com.akuity.kargo.api.v1alpha1.Promot * Use `create(PromotionListSchema)` to create a new message. */ export const PromotionListSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 35); + messageDesc(file_v1alpha1_generated, 36); /** * PromotionPolicy defines policies governing the promotion of Freight to a @@ -1727,7 +1774,7 @@ export type PromotionPolicy = Message<"github.com.akuity.kargo.api.v1alpha1.Prom * Use `create(PromotionPolicySchema)` to create a new message. */ export const PromotionPolicySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 36); + messageDesc(file_v1alpha1_generated, 37); /** * PromotionReference contains the relevant information about a Promotion @@ -1770,7 +1817,7 @@ export type PromotionReference = Message<"github.com.akuity.kargo.api.v1alpha1.P * Use `create(PromotionReferenceSchema)` to create a new message. */ export const PromotionReferenceSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 37); + messageDesc(file_v1alpha1_generated, 38); /** * PromotionSpec describes the desired transition of a specific Stage into a @@ -1824,7 +1871,7 @@ export type PromotionSpec = Message<"github.com.akuity.kargo.api.v1alpha1.Promot * Use `create(PromotionSpecSchema)` to create a new message. */ export const PromotionSpecSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 38); + messageDesc(file_v1alpha1_generated, 39); /** * PromotionStatus describes the current state of the transition represented by @@ -1922,7 +1969,7 @@ export type PromotionStatus = Message<"github.com.akuity.kargo.api.v1alpha1.Prom * Use `create(PromotionStatusSchema)` to create a new message. */ export const PromotionStatusSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 39); + messageDesc(file_v1alpha1_generated, 40); /** * PromotionStep describes a directive to be executed as part of a Promotion. @@ -1969,7 +2016,7 @@ export type PromotionStep = Message<"github.com.akuity.kargo.api.v1alpha1.Promot * Use `create(PromotionStepSchema)` to create a new message. */ export const PromotionStepSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 40); + messageDesc(file_v1alpha1_generated, 41); /** * PromotionStepRetry describes the retry policy for a PromotionStep. @@ -2028,7 +2075,7 @@ export type PromotionStepRetry = Message<"github.com.akuity.kargo.api.v1alpha1.P * Use `create(PromotionStepRetrySchema)` to create a new message. */ export const PromotionStepRetrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 41); + messageDesc(file_v1alpha1_generated, 42); /** * PromotionTemplate defines a template for a Promotion that can be used to @@ -2048,7 +2095,7 @@ export type PromotionTemplate = Message<"github.com.akuity.kargo.api.v1alpha1.Pr * Use `create(PromotionTemplateSchema)` to create a new message. */ export const PromotionTemplateSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 42); + messageDesc(file_v1alpha1_generated, 43); /** * PromotionTemplateSpec describes the (partial) specification of a Promotion @@ -2083,7 +2130,7 @@ export type PromotionTemplateSpec = Message<"github.com.akuity.kargo.api.v1alpha * Use `create(PromotionTemplateSpecSchema)` to create a new message. */ export const PromotionTemplateSpecSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 43); + messageDesc(file_v1alpha1_generated, 44); /** * PromotionVariable describes a single variable that may be referenced by @@ -2117,7 +2164,7 @@ export type PromotionVariable = Message<"github.com.akuity.kargo.api.v1alpha1.Pr * Use `create(PromotionVariableSchema)` to create a new message. */ export const PromotionVariableSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 44); + messageDesc(file_v1alpha1_generated, 45); /** * RepoSubscription describes a subscription to ONE OF a Git repository, a @@ -2153,7 +2200,7 @@ export type RepoSubscription = Message<"github.com.akuity.kargo.api.v1alpha1.Rep * Use `create(RepoSubscriptionSchema)` to create a new message. */ export const RepoSubscriptionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 45); + messageDesc(file_v1alpha1_generated, 46); /** * Stage is the Kargo API's main type. @@ -2189,7 +2236,7 @@ export type Stage = Message<"github.com.akuity.kargo.api.v1alpha1.Stage"> & { * Use `create(StageSchema)` to create a new message. */ export const StageSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 46); + messageDesc(file_v1alpha1_generated, 47); /** * StageList is a list of Stage resources. @@ -2213,7 +2260,7 @@ export type StageList = Message<"github.com.akuity.kargo.api.v1alpha1.StageList" * Use `create(StageListSchema)` to create a new message. */ export const StageListSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 47); + messageDesc(file_v1alpha1_generated, 48); /** * StageSpec describes the sources of Freight used by a Stage and how to @@ -2271,7 +2318,7 @@ export type StageSpec = Message<"github.com.akuity.kargo.api.v1alpha1.StageSpec" * Use `create(StageSpecSchema)` to create a new message. */ export const StageSpecSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 48); + messageDesc(file_v1alpha1_generated, 49); /** * StageStatus describes a Stages's current and recent Freight, health, and @@ -2378,7 +2425,7 @@ export type StageStatus = Message<"github.com.akuity.kargo.api.v1alpha1.StageSta * Use `create(StageStatusSchema)` to create a new message. */ export const StageStatusSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 49); + messageDesc(file_v1alpha1_generated, 50); /** * StepExecutionMetadata tracks metadata pertaining to the execution of @@ -2437,7 +2484,7 @@ export type StepExecutionMetadata = Message<"github.com.akuity.kargo.api.v1alpha * Use `create(StepExecutionMetadataSchema)` to create a new message. */ export const StepExecutionMetadataSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 50); + messageDesc(file_v1alpha1_generated, 51); /** * Verification describes how to verify that a Promotion has been successful @@ -2476,7 +2523,7 @@ export type Verification = Message<"github.com.akuity.kargo.api.v1alpha1.Verific * Use `create(VerificationSchema)` to create a new message. */ export const VerificationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 51); + messageDesc(file_v1alpha1_generated, 52); /** * VerificationInfo contains the details of an instance of a Verification @@ -2546,7 +2593,7 @@ export type VerificationInfo = Message<"github.com.akuity.kargo.api.v1alpha1.Ver * Use `create(VerificationInfoSchema)` to create a new message. */ export const VerificationInfoSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 52); + messageDesc(file_v1alpha1_generated, 53); /** * VerifiedStage describes a Stage in which Freight has been verified. @@ -2560,6 +2607,17 @@ export type VerifiedStage = Message<"github.com.akuity.kargo.api.v1alpha1.Verifi * @generated from field: optional k8s.io.apimachinery.pkg.apis.meta.v1.Time verifiedAt = 1; */ verifiedAt?: Time; + + /** + * LongestCompletedSoak represents the longest definite time interval wherein + * the Freight was in CONTINUOUS use by the Stage. This value is updated as + * Freight EXITS the Stage. If the Freight is currently in use by the Stage, + * the time elapsed since the Freight ENTERED the Stage is its current soak + * time, which may exceed the value of this field. + * + * @generated from field: optional k8s.io.apimachinery.pkg.apis.meta.v1.Duration longestSoak = 2; + */ + longestSoak?: Duration; }; /** @@ -2567,7 +2625,7 @@ export type VerifiedStage = Message<"github.com.akuity.kargo.api.v1alpha1.Verifi * Use `create(VerifiedStageSchema)` to create a new message. */ export const VerifiedStageSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 53); + messageDesc(file_v1alpha1_generated, 54); /** * Warehouse is a source of Freight. @@ -2602,7 +2660,7 @@ export type Warehouse = Message<"github.com.akuity.kargo.api.v1alpha1.Warehouse" * Use `create(WarehouseSchema)` to create a new message. */ export const WarehouseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 54); + messageDesc(file_v1alpha1_generated, 55); /** * WarehouseList is a list of Warehouse resources. @@ -2626,7 +2684,7 @@ export type WarehouseList = Message<"github.com.akuity.kargo.api.v1alpha1.Wareho * Use `create(WarehouseListSchema)` to create a new message. */ export const WarehouseListSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 55); + messageDesc(file_v1alpha1_generated, 56); /** * WarehouseSpec describes sources of versioned artifacts to be included in @@ -2692,7 +2750,7 @@ export type WarehouseSpec = Message<"github.com.akuity.kargo.api.v1alpha1.Wareho * Use `create(WarehouseSpecSchema)` to create a new message. */ export const WarehouseSpecSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 56); + messageDesc(file_v1alpha1_generated, 57); /** * WarehouseStatus describes a Warehouse's most recently observed state. @@ -2751,5 +2809,5 @@ export type WarehouseStatus = Message<"github.com.akuity.kargo.api.v1alpha1.Ware * Use `create(WarehouseStatusSchema)` to create a new message. */ export const WarehouseStatusSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1alpha1_generated, 57); + messageDesc(file_v1alpha1_generated, 58);