Skip to content

Commit

Permalink
Merge pull request #190 from utilitywarehouse/as-annotation-trigger
Browse files Browse the repository at this point in the history
add max concurrent module run limit and trigger runs by annotation
  • Loading branch information
asiyani authored Apr 12, 2024
2 parents 4c817a6 + 65ff04a commit 4007136
Show file tree
Hide file tree
Showing 21 changed files with 740 additions and 178 deletions.
48 changes: 47 additions & 1 deletion api/v1beta1/module_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,30 @@ limitations under the License.
package v1beta1

import (
"encoding/json"
"strings"
"time"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)

// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

const (
RunRequestAnnotationKey = `terraform-applier.uw.systems/run-request`
)

// The potential reasons for events and current state
const (
ReasonRunTriggered = "RunTriggered"
ReasonForcedPlanTriggered = "ForcedPlanTriggered"
ReasonForcedApplyTriggered = "ForcedApplyTriggered"
ReasonPollingRunTriggered = "PollingRunTriggered"
ReasonScheduledRunTriggered = "ScheduledRunTriggered"
ReasonPRPlanTriggered = "PullRequestPlanTriggered"

ReasonRunPreparationFailed = "RunPreparationFailed"
ReasonDelegationFailed = "DelegationFailed"
Expand Down Expand Up @@ -62,6 +70,8 @@ const (
ForcedPlan = "ForcedPlan"
// ForcedApply indicates a forced (triggered on the UI) terraform apply.
ForcedApply = "ForcedApply"
// PRPlan indicates terraform plan trigged by PullRequest on modules repo path.
PRPlan = "PullRequestPlan"
)

// Overall state of Module run
Expand Down Expand Up @@ -306,7 +316,41 @@ func (m *Module) IsPlanOnly() bool {
return m.Spec.PlanOnly != nil && *m.Spec.PlanOnly
}

func GetRunReason(runType string) string {
func (m *Module) NewRunRequest(reqType string) *Request {
req := Request{
NamespacedName: types.NamespacedName{
Namespace: m.Namespace,
Name: m.Name,
},
RequestedAt: &metav1.Time{Time: time.Now()},
ID: NewRequestID(),
Type: reqType,
}

return &req
}

// PendingRunRequest returns pending requests if any from module's annotation.
func (m *Module) PendingRunRequest() (*Request, bool) {
valueString, exists := m.ObjectMeta.Annotations[RunRequestAnnotationKey]
if !exists {
return nil, false
}
value := Request{}
if err := json.Unmarshal([]byte(valueString), &value); err != nil {
// unmarshal errors are ignored as it should not happen and if it does
// it can be treated as no request pending and module can override it
// with new valid request
return nil, false
}
value.NamespacedName = types.NamespacedName{
Namespace: m.Namespace,
Name: m.Name,
}
return &value, true
}

func RunReason(runType string) string {
switch runType {
case ScheduledRun:
return ReasonScheduledRunTriggered
Expand All @@ -316,6 +360,8 @@ func GetRunReason(runType string) string {
return ReasonForcedPlanTriggered
case ForcedApply:
return ReasonForcedApplyTriggered
case PRPlan:
return ReasonPRPlanTriggered
}
return ReasonRunTriggered
}
Expand Down
96 changes: 96 additions & 0 deletions api/v1beta1/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package v1beta1

import (
"crypto/rand"
"encoding/base64"
"fmt"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)

var (
ErrRunRequestExist = fmt.Errorf("another pending run request found")
ErrNoRunRequestFound = fmt.Errorf("no pending run requests found")
ErrRunRequestMismatch = fmt.Errorf("run request ID doesn't match pending request id")
)

// Request represents terraform run request
type Request struct {
NamespacedName types.NamespacedName `json:"-"`
ID string `json:"id,omitempty"`
RequestedAt *metav1.Time `json:"reqAt,omitempty"`
Type string `json:"type,omitempty"`
PR *PullRequest `json:"pr,omitempty"`
}

type PullRequest struct {
Number int `json:"num,omitempty"`
HeadBranch string `json:"headBranch,omitempty"`
CommentID string `json:"commentID,omitempty"`
}

func (req *Request) Validate() error {
if req.NamespacedName.Namespace == "" {
return fmt.Errorf("namespace is required")
}
if req.NamespacedName.Name == "" {
return fmt.Errorf("name is required")
}
if req.RequestedAt.IsZero() {
return fmt.Errorf("valid timestamp is required for 'RequestedAt'")
}

switch req.Type {
case ScheduledRun,
PollingRun,
ForcedPlan,
ForcedApply,
PRPlan:
default:
return fmt.Errorf("unknown Request type provided")
}

return nil
}

// IsPlanOnly will return is req is plan-only
func (req *Request) IsPlanOnly(module *Module) bool {
// for scheduled and polling run respect module spec
if req.Type == ScheduledRun ||
req.Type == PollingRun {
return module.IsPlanOnly()
}

// this is override triggered by user
if req.Type == ForcedApply {
return false
}

// these are plan only override requests
if req.Type == PRPlan ||
req.Type == ForcedPlan {
return true
}

// its always safe to default to plan-only
return true
}

// RepoRef returns the revision of the repository for the module source code
// based on request type
func (req *Request) RepoRef(module *Module) string {
// this is override triggered by user
if req.Type == PRPlan {
return req.PR.HeadBranch
}

return module.Spec.RepoRef
}

// NewRequestID generates random string as ID
func NewRequestID() string {
b := make([]byte, 6)
rand.Read(b)
return base64.StdEncoding.EncodeToString(b)
}
40 changes: 40 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 4007136

Please sign in to comment.