github.com/redhat-appstudio/release-service@v0.0.0-20240507045911-a8558ef3422a/api/v1alpha1/release_types.go (about)

     1  /*
     2  Copyright 2022.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package v1alpha1
    18  
    19  import (
    20  	"time"
    21  
    22  	"github.com/konflux-ci/operator-toolkit/conditions"
    23  
    24  	"github.com/redhat-appstudio/release-service/metrics"
    25  	"k8s.io/apimachinery/pkg/runtime"
    26  
    27  	"k8s.io/apimachinery/pkg/api/meta"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  )
    30  
    31  // ReleaseSpec defines the desired state of Release.
    32  type ReleaseSpec struct {
    33  	// Snapshot to be released
    34  	// +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
    35  	// +required
    36  	Snapshot string `json:"snapshot"`
    37  
    38  	// ReleasePlan to use for this particular Release
    39  	// +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
    40  	// +required
    41  	ReleasePlan string `json:"releasePlan"`
    42  
    43  	// Data is an unstructured key used for providing data for the managed Release Pipeline
    44  	// +kubebuilder:pruning:PreserveUnknownFields
    45  	// +optional
    46  	Data *runtime.RawExtension `json:"data,omitempty"`
    47  
    48  	// GracePeriodDays is the number of days a Release should be kept
    49  	// This value is used to define the Release ExpirationTime
    50  	// +optional
    51  	GracePeriodDays int `json:"gracePeriodDays,omitempty"`
    52  }
    53  
    54  // ReleaseStatus defines the observed state of Release.
    55  type ReleaseStatus struct {
    56  	// Attribution contains information about the entity authorizing the release
    57  	// +optional
    58  	Attribution AttributionInfo `json:"attribution,omitempty"`
    59  
    60  	// Conditions represent the latest available observations for the release
    61  	// +optional
    62  	Conditions []metav1.Condition `json:"conditions"`
    63  
    64  	// PostActionsExecution contains information about the post-actions execution
    65  	// +optional
    66  	PostActionsExecution PostActionsExecutionInfo `json:"postActionsExecution,omitempty"`
    67  
    68  	// Processing contains information about the release processing
    69  	// +optional
    70  	Processing ProcessingInfo `json:"processing,omitempty"`
    71  
    72  	// Validation contains information about the release validation
    73  	// +optional
    74  	Validation ValidationInfo `json:"validation,omitempty"`
    75  
    76  	// Target references where this release is intended to be released to
    77  	// +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
    78  	// +optional
    79  	Target string `json:"target,omitempty"`
    80  
    81  	// Automated indicates whether the Release was created as part of an automated process or manually by an end-user
    82  	// +optional
    83  	Automated bool `json:"automated,omitempty"`
    84  
    85  	// CompletionTime is the time when a Release was completed
    86  	// +optional
    87  	CompletionTime *metav1.Time `json:"completionTime,omitempty"`
    88  
    89  	// StartTime is the time when a Release started
    90  	// +optional
    91  	StartTime *metav1.Time `json:"startTime,omitempty"`
    92  
    93  	// ExpirationTime is the time when a Release can be purged
    94  	// +optional
    95  	ExpirationTime *metav1.Time `json:"expirationTime,omitempty"`
    96  }
    97  
    98  // AttributionInfo defines the observed state of the release attribution.
    99  type AttributionInfo struct {
   100  	// Author is the username that the release is attributed to
   101  	// +optional
   102  	Author string `json:"author,omitempty"`
   103  
   104  	// StandingAuthorization indicates whether the release is attributed through a ReleasePlan
   105  	// +optional
   106  	StandingAuthorization bool `json:"standingAuthorization,omitempty"`
   107  }
   108  
   109  // PostActionsExecutionInfo defines the observed state of the post-actions execution.
   110  type PostActionsExecutionInfo struct {
   111  	// CompletionTime is the time when the Release post-actions execution was completed
   112  	// +optional
   113  	CompletionTime *metav1.Time `json:"completionTime,omitempty"`
   114  
   115  	// StartTime is the time when the Release post-actions execution started
   116  	// +optional
   117  	StartTime *metav1.Time `json:"startTime,omitempty"`
   118  }
   119  
   120  // ProcessingInfo defines the observed state of the release processing.
   121  type ProcessingInfo struct {
   122  	// CompletionTime is the time when the Release processing was completed
   123  	// +optional
   124  	CompletionTime *metav1.Time `json:"completionTime,omitempty"`
   125  
   126  	// PipelineRun contains the namespaced name of the managed Release PipelineRun executed as part of this release
   127  	// +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?\/[a-z0-9]([-a-z0-9]*[a-z0-9])?$
   128  	// +optional
   129  	PipelineRun string `json:"pipelineRun,omitempty"`
   130  
   131  	// RoleBinding contains the namespaced name of the roleBinding created for the managed Release PipelineRun
   132  	// executed as part of this release
   133  	// +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?\/[a-z0-9]([-a-z0-9]*[a-z0-9])?$
   134  	// +optional
   135  	RoleBinding string `json:"roleBinding,omitempty"`
   136  
   137  	// StartTime is the time when the Release processing started
   138  	// +optional
   139  	StartTime *metav1.Time `json:"startTime,omitempty"`
   140  }
   141  
   142  // ValidationInfo defines the observed state of the release validation.
   143  type ValidationInfo struct {
   144  	// FailedPostValidation indicates whether the Release was marked as invalid after being initially marked as valid
   145  	FailedPostValidation bool `json:"failedPostValidation,omitempty"`
   146  
   147  	// Time is the time when the Release was validated or when the validation state changed
   148  	// +optional
   149  	Time *metav1.Time `json:"time,omitempty"`
   150  }
   151  
   152  // +kubebuilder:object:root=true
   153  // +kubebuilder:resource:shortName=rel
   154  // +kubebuilder:subresource:status
   155  // +kubebuilder:printcolumn:name="Snapshot",type=string,JSONPath=`.spec.snapshot`
   156  // +kubebuilder:printcolumn:name="ReleasePlan",type=string,JSONPath=`.spec.releasePlan`
   157  // +kubebuilder:printcolumn:name="Release status",type=string,JSONPath=`.status.conditions[?(@.type=="Released")].reason`
   158  // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
   159  
   160  // Release is the Schema for the releases API
   161  type Release struct {
   162  	metav1.TypeMeta   `json:",inline"`
   163  	metav1.ObjectMeta `json:"metadata,omitempty"`
   164  
   165  	Spec   ReleaseSpec   `json:"spec,omitempty"`
   166  	Status ReleaseStatus `json:"status,omitempty"`
   167  }
   168  
   169  // HasEveryPostActionExecutionFinished checks whether the Release post-actions execution has finished,
   170  // regardless of the result.
   171  func (r *Release) HasEveryPostActionExecutionFinished() bool {
   172  	return r.hasPhaseFinished(postActionsExecutedConditionType)
   173  }
   174  
   175  // HasProcessingFinished checks whether the Release processing has finished, regardless of the result.
   176  func (r *Release) HasProcessingFinished() bool {
   177  	return r.hasPhaseFinished(processedConditionType)
   178  }
   179  
   180  // HasReleaseFinished checks whether the Release has finished, regardless of the result.
   181  func (r *Release) HasReleaseFinished() bool {
   182  	return r.hasPhaseFinished(releasedConditionType)
   183  }
   184  
   185  // IsAttributed checks whether the Release was marked as attributed.
   186  func (r *Release) IsAttributed() bool {
   187  	return r.Status.Attribution.Author != ""
   188  }
   189  
   190  // IsAutomated checks whether the Release was marked as automated.
   191  func (r *Release) IsAutomated() bool {
   192  	return r.Status.Automated
   193  }
   194  
   195  // IsEveryPostActionExecuted checks whether the Release post-actions were successfully executed.
   196  func (r *Release) IsEveryPostActionExecuted() bool {
   197  	return meta.IsStatusConditionTrue(r.Status.Conditions, postActionsExecutedConditionType.String())
   198  }
   199  
   200  // IsEachPostActionExecuting checks whether the Release post-actions are in progress.
   201  func (r *Release) IsEachPostActionExecuting() bool {
   202  	return r.isPhaseProgressing(postActionsExecutedConditionType)
   203  }
   204  
   205  // IsProcessed checks whether the Release was successfully processed.
   206  func (r *Release) IsProcessed() bool {
   207  	return meta.IsStatusConditionTrue(r.Status.Conditions, processedConditionType.String())
   208  }
   209  
   210  // IsProcessing checks whether the Release processing is in progress.
   211  func (r *Release) IsProcessing() bool {
   212  	return r.isPhaseProgressing(processedConditionType)
   213  }
   214  
   215  // IsReleased checks whether the Release has finished successfully.
   216  func (r *Release) IsReleased() bool {
   217  	return meta.IsStatusConditionTrue(r.Status.Conditions, releasedConditionType.String())
   218  }
   219  
   220  // IsReleasing checks whether the Release is in progress.
   221  func (r *Release) IsReleasing() bool {
   222  	return r.isPhaseProgressing(releasedConditionType)
   223  }
   224  
   225  // IsValid checks whether the Release validation has finished successfully.
   226  func (r *Release) IsValid() bool {
   227  	return meta.IsStatusConditionTrue(r.Status.Conditions, validatedConditionType.String())
   228  }
   229  
   230  // MarkProcessed marks the Release as processed.
   231  func (r *Release) MarkProcessed() {
   232  	if !r.IsProcessing() || r.HasProcessingFinished() {
   233  		return
   234  	}
   235  
   236  	r.Status.Processing.CompletionTime = &metav1.Time{Time: time.Now()}
   237  	conditions.SetCondition(&r.Status.Conditions, processedConditionType, metav1.ConditionTrue, SucceededReason)
   238  
   239  	go metrics.RegisterCompletedReleaseProcessing(
   240  		r.Status.Processing.StartTime,
   241  		r.Status.Processing.CompletionTime,
   242  		SucceededReason.String(),
   243  		r.Status.Target,
   244  	)
   245  }
   246  
   247  // MarkProcessing marks the Release as processing.
   248  func (r *Release) MarkProcessing(message string) {
   249  	if r.HasProcessingFinished() {
   250  		return
   251  	}
   252  
   253  	if !r.IsProcessing() {
   254  		r.Status.Processing.StartTime = &metav1.Time{Time: time.Now()}
   255  	}
   256  
   257  	conditions.SetConditionWithMessage(&r.Status.Conditions, processedConditionType, metav1.ConditionFalse, ProgressingReason, message)
   258  
   259  	go metrics.RegisterNewReleaseProcessing(
   260  		r.Status.Processing.StartTime,
   261  		r.Status.StartTime,
   262  		ProgressingReason.String(),
   263  		r.Status.Target,
   264  	)
   265  }
   266  
   267  // MarkProcessingFailed marks the Release processing as failed.
   268  func (r *Release) MarkProcessingFailed(message string) {
   269  	if !r.IsProcessing() || r.HasProcessingFinished() {
   270  		return
   271  	}
   272  
   273  	r.Status.Processing.CompletionTime = &metav1.Time{Time: time.Now()}
   274  	conditions.SetConditionWithMessage(&r.Status.Conditions, processedConditionType, metav1.ConditionFalse, FailedReason, message)
   275  
   276  	go metrics.RegisterCompletedReleaseProcessing(
   277  		r.Status.Processing.StartTime,
   278  		r.Status.Processing.CompletionTime,
   279  		FailedReason.String(),
   280  		r.Status.Target,
   281  	)
   282  }
   283  
   284  // MarkPostActionsExecuted marks the Release post-actions as executed.
   285  func (r *Release) MarkPostActionsExecuted() {
   286  	if !r.IsEachPostActionExecuting() || r.HasEveryPostActionExecutionFinished() {
   287  		return
   288  	}
   289  
   290  	r.Status.PostActionsExecution.CompletionTime = &metav1.Time{Time: time.Now()}
   291  	conditions.SetCondition(&r.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionTrue, SucceededReason)
   292  
   293  	go metrics.RegisterCompletedReleasePostActionsExecuted(
   294  		r.Status.PostActionsExecution.StartTime,
   295  		r.Status.PostActionsExecution.CompletionTime,
   296  		SucceededReason.String(),
   297  	)
   298  }
   299  
   300  // MarkPostActionsExecuting marks the Release post-actions as executing.
   301  func (r *Release) MarkPostActionsExecuting(message string) {
   302  	if r.HasEveryPostActionExecutionFinished() {
   303  		return
   304  	}
   305  
   306  	if !r.IsEachPostActionExecuting() {
   307  		r.Status.PostActionsExecution.StartTime = &metav1.Time{Time: time.Now()}
   308  	}
   309  
   310  	conditions.SetConditionWithMessage(&r.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionFalse, ProgressingReason, message)
   311  
   312  	go metrics.RegisterNewReleasePostActionsExecution()
   313  }
   314  
   315  // MarkPostActionsExecutionFailed marks the Release post-actions execution as failed.
   316  func (r *Release) MarkPostActionsExecutionFailed(message string) {
   317  	if !r.IsEachPostActionExecuting() || r.HasEveryPostActionExecutionFinished() {
   318  		return
   319  	}
   320  
   321  	r.Status.PostActionsExecution.CompletionTime = &metav1.Time{Time: time.Now()}
   322  	conditions.SetConditionWithMessage(&r.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionFalse, FailedReason, message)
   323  
   324  	go metrics.RegisterCompletedReleasePostActionsExecuted(
   325  		r.Status.Processing.StartTime,
   326  		r.Status.Processing.CompletionTime,
   327  		FailedReason.String(),
   328  	)
   329  }
   330  
   331  // MarkReleased marks the Release as released.
   332  func (r *Release) MarkReleased() {
   333  	if !r.IsReleasing() || r.HasReleaseFinished() {
   334  		return
   335  	}
   336  
   337  	r.Status.CompletionTime = &metav1.Time{Time: time.Now()}
   338  	conditions.SetCondition(&r.Status.Conditions, releasedConditionType, metav1.ConditionTrue, SucceededReason)
   339  
   340  	go metrics.RegisterCompletedRelease(
   341  		r.Status.StartTime,
   342  		r.Status.CompletionTime,
   343  		r.getPhaseReason(postActionsExecutedConditionType),
   344  		r.getPhaseReason(processedConditionType),
   345  		SucceededReason.String(),
   346  		r.Status.Target,
   347  		r.getPhaseReason(validatedConditionType),
   348  	)
   349  }
   350  
   351  // MarkReleasing marks the Release as releasing.
   352  func (r *Release) MarkReleasing(message string) {
   353  	if r.HasReleaseFinished() {
   354  		return
   355  	}
   356  
   357  	if !r.IsReleasing() {
   358  		r.Status.StartTime = &metav1.Time{Time: time.Now()}
   359  	}
   360  
   361  	conditions.SetConditionWithMessage(&r.Status.Conditions, releasedConditionType, metav1.ConditionFalse, ProgressingReason, message)
   362  
   363  	go metrics.RegisterNewRelease()
   364  }
   365  
   366  // MarkReleaseFailed marks the Release as failed.
   367  func (r *Release) MarkReleaseFailed(message string) {
   368  	if !r.IsReleasing() || r.HasReleaseFinished() {
   369  		return
   370  	}
   371  
   372  	r.Status.CompletionTime = &metav1.Time{Time: time.Now()}
   373  	conditions.SetConditionWithMessage(&r.Status.Conditions, releasedConditionType, metav1.ConditionFalse, FailedReason, message)
   374  
   375  	go metrics.RegisterCompletedRelease(
   376  		r.Status.StartTime,
   377  		r.Status.CompletionTime,
   378  		r.getPhaseReason(postActionsExecutedConditionType),
   379  		r.getPhaseReason(processedConditionType),
   380  		FailedReason.String(),
   381  		r.Status.Target,
   382  		r.getPhaseReason(validatedConditionType),
   383  	)
   384  }
   385  
   386  // MarkValidated marks the Release as validated.
   387  func (r *Release) MarkValidated() {
   388  	if r.IsValid() {
   389  		return
   390  	}
   391  
   392  	r.Status.Validation.Time = &metav1.Time{Time: time.Now()}
   393  	conditions.SetCondition(&r.Status.Conditions, validatedConditionType, metav1.ConditionTrue, SucceededReason)
   394  
   395  	go metrics.RegisterValidatedRelease(
   396  		r.Status.StartTime,
   397  		r.Status.Validation.Time,
   398  		SucceededReason.String(),
   399  		r.Status.Target,
   400  	)
   401  }
   402  
   403  // MarkValidationFailed marks the Release validation as failed.
   404  func (r *Release) MarkValidationFailed(message string) {
   405  	if r.IsValid() {
   406  		r.Status.Validation.FailedPostValidation = true
   407  	}
   408  
   409  	r.Status.Validation.Time = &metav1.Time{Time: time.Now()}
   410  	conditions.SetConditionWithMessage(&r.Status.Conditions, validatedConditionType, metav1.ConditionFalse, FailedReason, message)
   411  
   412  	go metrics.RegisterValidatedRelease(
   413  		r.Status.StartTime,
   414  		r.Status.Validation.Time,
   415  		FailedReason.String(),
   416  		r.Status.Target,
   417  	)
   418  }
   419  
   420  // SetAutomated marks the Release as automated.
   421  func (r *Release) SetAutomated() {
   422  	if r.IsAutomated() {
   423  		return
   424  	}
   425  
   426  	r.Status.Automated = true
   427  }
   428  
   429  // SetExpirationTime set the time when this release can be purged
   430  func (r *Release) SetExpirationTime(expireDays time.Duration) {
   431  	creationTime := r.CreationTimestamp
   432  	r.Status.ExpirationTime = &metav1.Time{Time: creationTime.Add(time.Hour * 24 * expireDays)}
   433  }
   434  
   435  // getPhaseReason returns the current reason for the given ConditionType or empty string if no condition is found.
   436  func (r *Release) getPhaseReason(conditionType conditions.ConditionType) string {
   437  	var reason string
   438  
   439  	condition := meta.FindStatusCondition(r.Status.Conditions, conditionType.String())
   440  	if condition != nil {
   441  		reason = condition.Reason
   442  	}
   443  
   444  	return reason
   445  }
   446  
   447  // hasPhaseFinished checks whether a Release phase (e.g. deployment or processing) has finished.
   448  func (r *Release) hasPhaseFinished(conditionType conditions.ConditionType) bool {
   449  	condition := meta.FindStatusCondition(r.Status.Conditions, conditionType.String())
   450  
   451  	switch {
   452  	case condition == nil:
   453  		return false
   454  	case condition.Status == metav1.ConditionTrue:
   455  		return true
   456  	default:
   457  		return condition.Status == metav1.ConditionFalse && condition.Reason != ProgressingReason.String()
   458  	}
   459  }
   460  
   461  // isPhaseProgressing checks whether a Release phase (e.g. deployment or processing) is progressing.
   462  func (r *Release) isPhaseProgressing(conditionType conditions.ConditionType) bool {
   463  	condition := meta.FindStatusCondition(r.Status.Conditions, conditionType.String())
   464  
   465  	switch {
   466  	case condition == nil:
   467  		return false
   468  	case condition.Status == metav1.ConditionTrue:
   469  		return false
   470  	default:
   471  		return condition.Status == metav1.ConditionFalse && condition.Reason == ProgressingReason.String()
   472  	}
   473  }
   474  
   475  // +kubebuilder:object:root=true
   476  
   477  // ReleaseList contains a list of Release
   478  type ReleaseList struct {
   479  	metav1.TypeMeta `json:",inline"`
   480  	metav1.ListMeta `json:"metadata,omitempty"`
   481  	Items           []Release `json:"items"`
   482  }
   483  
   484  func init() {
   485  	SchemeBuilder.Register(&Release{}, &ReleaseList{})
   486  }