Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add max concurrent module run limit and trigger runs by annotation #190

Merged
merged 15 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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