diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index aee224001..fba022da8 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -11,7 +11,6 @@ builds:
- -X ${PACKAGE}/internal/version.BuildTimestamp=${BUILD_TIMESTAMP}
goos:
- linux
- - darwin
main: ./
binary: burrito
archives:
diff --git a/Makefile b/Makefile
index dfbaea4ec..6955789b9 100644
--- a/Makefile
+++ b/Makefile
@@ -180,7 +180,7 @@ ENVTEST ?= $(LOCALBIN)/setup-envtest
## Tool Versions
KUSTOMIZE_VERSION ?= v3.8.7
-CONTROLLER_TOOLS_VERSION ?= v0.10.0
+CONTROLLER_TOOLS_VERSION ?= v0.11.2
KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"
.PHONY: kustomize
diff --git a/api/v1alpha1/terraformlayer_types.go b/api/v1alpha1/terraformlayer_types.go
index b0185ef17..531e1151b 100644
--- a/api/v1alpha1/terraformlayer_types.go
+++ b/api/v1alpha1/terraformlayer_types.go
@@ -33,12 +33,10 @@ type TerraformLayerSpec struct {
TerraformConfig TerraformConfig `json:"terraform,omitempty"`
Repository TerraformLayerRepository `json:"repository,omitempty"`
RemediationStrategy RemediationStrategy `json:"remediationStrategy,omitempty"`
- PlanOnPullRequest bool `json:"planOnPullRequest,omitempty"`
OverrideRunnerSpec OverrideRunnerSpec `json:"overrideRunnerSpec,omitempty"`
}
type TerraformLayerRepository struct {
- Kind string `json:"kind,omitempty"`
Name string `json:"name,omitempty"`
Namespace string `json:"namespace,omitempty"`
}
diff --git a/api/v1alpha1/terraformpullrequest_types.go b/api/v1alpha1/terraformpullrequest_types.go
new file mode 100644
index 000000000..1d3730cf3
--- /dev/null
+++ b/api/v1alpha1/terraformpullrequest_types.go
@@ -0,0 +1,69 @@
+/*
+Copyright 2022.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// 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.
+
+// TerraformPullRequestSpec defines the desired state of TerraformPullRequest
+type TerraformPullRequestSpec struct {
+ Provider string `json:"provider,omitempty"`
+ Branch string `json:"branch,omitempty"`
+ Base string `json:"base,omitempty"`
+ ID string `json:"id,omitempty"`
+ Repository TerraformLayerRepository `json:"repository,omitempty"`
+}
+
+// TerraformPullRequestStatus defines the observed state of TerraformPullRequest
+type TerraformPullRequestStatus struct {
+ Conditions []metav1.Condition `json:"conditions,omitempty"`
+ State string `json:"state,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:resource:shortName=pr;prs;pullrequest;pullrequests;
+// +kubebuilder:subresource:status
+// +kubebuilder:printcolumn:name="ID",type=string,JSONPath=`.spec.id`
+// +kubebuilder:printcolumn:name="State",type=string,JSONPath=`.status.state`
+// +kubebuilder:printcolumn:name="Provider",type=string,JSONPath=`.spec.provider`
+// +kubebuilder:printcolumn:name="Base",type=string,JSONPath=`.spec.base`
+// +kubebuilder:printcolumn:name="Branch",type=string,JSONPath=`.spec.branch`
+// TerraformPullRequest is the Schema for the TerraformPullRequests API
+type TerraformPullRequest struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ Spec TerraformPullRequestSpec `json:"spec,omitempty"`
+ Status TerraformPullRequestStatus `json:"status,omitempty"`
+}
+
+//+kubebuilder:object:root=true
+
+// TerraformPullRequestList contains a list of TerraformPullRequest
+type TerraformPullRequestList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty"`
+ Items []TerraformPullRequest `json:"items"`
+}
+
+func init() {
+ SchemeBuilder.Register(&TerraformPullRequest{}, &TerraformPullRequestList{})
+}
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index ee1987074..304aa2650 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -250,6 +250,103 @@ func (in *TerraformLayerStatus) DeepCopy() *TerraformLayerStatus {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TerraformPullRequest) DeepCopyInto(out *TerraformPullRequest) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ out.Spec = in.Spec
+ in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TerraformPullRequest.
+func (in *TerraformPullRequest) DeepCopy() *TerraformPullRequest {
+ if in == nil {
+ return nil
+ }
+ out := new(TerraformPullRequest)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *TerraformPullRequest) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TerraformPullRequestList) DeepCopyInto(out *TerraformPullRequestList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]TerraformPullRequest, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TerraformPullRequestList.
+func (in *TerraformPullRequestList) DeepCopy() *TerraformPullRequestList {
+ if in == nil {
+ return nil
+ }
+ out := new(TerraformPullRequestList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *TerraformPullRequestList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TerraformPullRequestSpec) DeepCopyInto(out *TerraformPullRequestSpec) {
+ *out = *in
+ out.Repository = in.Repository
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TerraformPullRequestSpec.
+func (in *TerraformPullRequestSpec) DeepCopy() *TerraformPullRequestSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(TerraformPullRequestSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TerraformPullRequestStatus) DeepCopyInto(out *TerraformPullRequestStatus) {
+ *out = *in
+ if in.Conditions != nil {
+ in, out := &in.Conditions, &out.Conditions
+ *out = make([]metav1.Condition, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TerraformPullRequestStatus.
+func (in *TerraformPullRequestStatus) DeepCopy() *TerraformPullRequestStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(TerraformPullRequestStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TerraformRepository) DeepCopyInto(out *TerraformRepository) {
*out = *in
diff --git a/cmd/controllers/start.go b/cmd/controllers/start.go
index 846eb56c2..2505073b0 100644
--- a/cmd/controllers/start.go
+++ b/cmd/controllers/start.go
@@ -27,7 +27,7 @@ func buildControllersStartCmd(app *burrito.App) *cobra.Command {
defaultOnErrorTimer, _ := time.ParseDuration("1m")
defaultWaitActionTimer, _ := time.ParseDuration("1m")
- cmd.Flags().StringSliceVar(&app.Config.Controller.Types, "types", []string{"layer", "repository"}, "list of controllers to start")
+ cmd.Flags().StringSliceVar(&app.Config.Controller.Types, "types", []string{"layer", "repository", "pullrequest"}, "list of controllers to start")
cmd.Flags().DurationVar(&app.Config.Controller.Timers.DriftDetection, "drift-detection-period", defaultDriftDetectionTimer, "period between two plans. Must end with s, m or h.")
cmd.Flags().DurationVar(&app.Config.Controller.Timers.OnError, "on-error-period", defaultOnErrorTimer, "period between two runners launch when an error occurred. Must end with s, m or h.")
diff --git a/config/crd/bases/config.terraform.padok.cloud_terraformlayers.yaml b/config/crd/bases/config.terraform.padok.cloud_terraformlayers.yaml
index 1275c204b..3c9ff5237 100644
--- a/config/crd/bases/config.terraform.padok.cloud_terraformlayers.yaml
+++ b/config/crd/bases/config.terraform.padok.cloud_terraformlayers.yaml
@@ -259,7 +259,9 @@ spec:
- name
type: object
type: array
- x-kubernetes-list-type: set
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
limits:
additionalProperties:
anyOf:
@@ -965,7 +967,9 @@ spec:
- name
type: object
type: array
- x-kubernetes-list-type: set
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
limits:
additionalProperties:
anyOf:
@@ -1983,8 +1987,6 @@ spec:
type: object
path:
type: string
- planOnPullRequest:
- type: boolean
remediationStrategy:
enum:
- dry
@@ -1992,8 +1994,6 @@ spec:
type: string
repository:
properties:
- kind:
- type: string
name:
type: string
namespace:
diff --git a/config/crd/bases/config.terraform.padok.cloud_terraformpullrequests.yaml b/config/crd/bases/config.terraform.padok.cloud_terraformpullrequests.yaml
new file mode 100644
index 000000000..d9747bfe3
--- /dev/null
+++ b/config/crd/bases/config.terraform.padok.cloud_terraformpullrequests.yaml
@@ -0,0 +1,155 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.10.0
+ creationTimestamp: null
+ name: terraformpullrequests.config.terraform.padok.cloud
+spec:
+ group: config.terraform.padok.cloud
+ names:
+ kind: TerraformPullRequest
+ listKind: TerraformPullRequestList
+ plural: terraformpullrequests
+ shortNames:
+ - pr
+ - prs
+ - pullrequest
+ - pullrequests
+ singular: terraformpullrequest
+ scope: Namespaced
+ versions:
+ - additionalPrinterColumns:
+ - jsonPath: .spec.id
+ name: ID
+ type: string
+ - jsonPath: .status.state
+ name: State
+ type: string
+ - jsonPath: .spec.provider
+ name: Provider
+ type: string
+ - jsonPath: .spec.base
+ name: Base
+ type: string
+ - jsonPath: .spec.branch
+ name: Branch
+ type: string
+ name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: TerraformPullRequest is the Schema for the TerraformPullRequests
+ API
+ properties:
+ apiVersion:
+ description: 'APIVersion defines the versioned schema of this representation
+ of an object. Servers should convert recognized schemas to the latest
+ internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+ type: string
+ kind:
+ description: 'Kind is a string value representing the REST resource this
+ object represents. Servers may infer this from the endpoint the client
+ submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: TerraformPullRequestSpec defines the desired state of TerraformPullRequest
+ properties:
+ base:
+ type: string
+ branch:
+ type: string
+ id:
+ type: string
+ provider:
+ type: string
+ repository:
+ properties:
+ name:
+ type: string
+ namespace:
+ type: string
+ type: object
+ type: object
+ status:
+ description: TerraformPullRequestStatus defines the observed state of
+ TerraformPullRequest
+ properties:
+ conditions:
+ items:
+ description: "Condition contains details for one aspect of the current
+ state of this API Resource. --- This struct is intended for direct
+ use as an array at the field path .status.conditions. For example,
+ \n type FooStatus struct{ // Represents the observations of a
+ foo's current state. // Known .status.conditions.type are: \"Available\",
+ \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
+ // +listType=map // +listMapKey=type Conditions []metav1.Condition
+ `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
+ protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
+ properties:
+ lastTransitionTime:
+ description: lastTransitionTime is the last time the condition
+ transitioned from one status to another. This should be when
+ the underlying condition changed. If that is not known, then
+ using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: message is a human readable message indicating
+ details about the transition. This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: observedGeneration represents the .metadata.generation
+ that the condition was set based upon. For instance, if .metadata.generation
+ is currently 12, but the .status.conditions[x].observedGeneration
+ is 9, the condition is out of date with respect to the current
+ state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: reason contains a programmatic identifier indicating
+ the reason for the condition's last transition. Producers
+ of specific condition types may define expected values and
+ meanings for this field, and whether the values are considered
+ a guaranteed API. The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ --- Many .condition.type values are consistent across resources
+ like Available, but because arbitrary conditions can be useful
+ (see .node.status.conditions), the ability to deconflict is
+ important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
+ state:
+ type: string
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/config/crd/bases/config.terraform.padok.cloud_terraformrepositories.yaml b/config/crd/bases/config.terraform.padok.cloud_terraformrepositories.yaml
index 34e1a610a..a69bc5338 100644
--- a/config/crd/bases/config.terraform.padok.cloud_terraformrepositories.yaml
+++ b/config/crd/bases/config.terraform.padok.cloud_terraformrepositories.yaml
@@ -247,7 +247,9 @@ spec:
- name
type: object
type: array
- x-kubernetes-list-type: set
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
limits:
additionalProperties:
anyOf:
@@ -953,7 +955,9 @@ spec:
- name
type: object
type: array
- x-kubernetes-list-type: set
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
limits:
additionalProperties:
anyOf:
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 693979b67..4988a78a8 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -31,6 +31,32 @@ rules:
- get
- patch
- update
+- apiGroups:
+ - config.terraform.padok.cloud
+ resources:
+ - terraformpullrequests
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - config.terraform.padok.cloud
+ resources:
+ - terraformpullrequests/finalizers
+ verbs:
+ - update
+- apiGroups:
+ - config.terraform.padok.cloud
+ resources:
+ - terraformpullrequests/status
+ verbs:
+ - get
+ - patch
+ - update
- apiGroups:
- config.terraform.padok.cloud
resources:
diff --git a/go.mod b/go.mod
index 07d49841a..1036a214b 100644
--- a/go.mod
+++ b/go.mod
@@ -3,25 +3,28 @@ module github.com/padok-team/burrito
go 1.19
require (
- github.com/go-playground/webhooks/v6 v6.0.1
- github.com/onsi/ginkgo/v2 v2.6.0
- github.com/onsi/gomega v1.24.1
+ github.com/hashicorp/go-multierror v1.1.1
+ github.com/hashicorp/terraform-json v0.14.0
+ github.com/onsi/ginkgo/v2 v2.6.1
+ github.com/onsi/gomega v1.24.2
github.com/sirupsen/logrus v1.8.1
- k8s.io/apimachinery v0.26.0
- k8s.io/client-go v0.26.0
+ k8s.io/apimachinery v0.26.1
+ k8s.io/client-go v0.26.1
sigs.k8s.io/controller-runtime v0.14.0
)
require (
github.com/Microsoft/go-winio v0.5.2 // indirect
- github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 // indirect
+ github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/cloudflare/circl v1.1.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
+ github.com/google/go-querystring v1.1.0 // indirect
+ github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
- github.com/hashicorp/terraform-json v0.14.0 // indirect
+ github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/pjbgf/sha1cd v0.2.3 // indirect
@@ -46,12 +49,14 @@ require (
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
+ github.com/go-playground/webhooks v5.17.0+incompatible
github.com/go-redis/redis/v8 v8.11.5
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
- github.com/golang/protobuf v1.5.2 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.9 // indirect
+ github.com/google/go-github/v50 v50.2.0
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/go-version v1.6.0
@@ -83,26 +88,27 @@ require (
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.14.0
github.com/subosito/gotenv v1.4.1 // indirect
+ github.com/xanzy/go-gitlab v0.81.0
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.24.0 // indirect
- golang.org/x/crypto v0.3.0 // indirect
- golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 // indirect
- golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
- golang.org/x/sys v0.3.0 // indirect
- golang.org/x/term v0.3.0 // indirect
- golang.org/x/text v0.5.0 // indirect
+ golang.org/x/crypto v0.7.0 // indirect
+ golang.org/x/net v0.8.0 // indirect
+ golang.org/x/oauth2 v0.6.0
+ golang.org/x/sys v0.6.0 // indirect
+ golang.org/x/term v0.6.0 // indirect
+ golang.org/x/text v0.8.0 // indirect
golang.org/x/time v0.3.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
- google.golang.org/protobuf v1.28.1 // indirect
+ google.golang.org/protobuf v1.29.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
- k8s.io/api v0.26.0
- k8s.io/apiextensions-apiserver v0.26.0 // indirect
- k8s.io/component-base v0.26.0 // indirect
+ k8s.io/api v0.26.1
+ k8s.io/apiextensions-apiserver v0.26.1 // indirect
+ k8s.io/component-base v0.26.1 // indirect
k8s.io/klog/v2 v2.80.1 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 // indirect
diff --git a/go.sum b/go.sum
index 0f60e61c9..a8dd384c8 100644
--- a/go.sum
+++ b/go.sum
@@ -43,8 +43,9 @@ github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugX
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
-github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 h1:ra2OtmuW0AE5csawV4YXMNGNQQXvLRps3z2Z59OPO+I=
github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
+github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
+github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -102,6 +103,7 @@ github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMi
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
+github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
@@ -145,12 +147,11 @@ github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXym
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng=
github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
-github.com/go-playground/webhooks/v6 v6.0.1 h1:ssqgU7vZ+xK+/Uwx4zkf5tfmzOHnLBpzSp5bJ4cX3rg=
-github.com/go-playground/webhooks/v6 v6.0.1/go.mod h1:GCocmfMtpJdkEOM1uG9p2nXzg1kY5X/LtvQgtPHUaaA=
+github.com/go-playground/webhooks v5.17.0+incompatible h1:Ea3zLJXlnlIFweIujDxdneq512xO4k9cYwAuZ3VuPJo=
+github.com/go-playground/webhooks v5.17.0+incompatible/go.mod h1:rMsxoY7bQzIPF9Ni55rTCyLG2af55f9IWgJ1ao3JiZA=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/gogits/go-gogs-client v0.0.0-20200905025246-8bb8a50cb355/go.mod h1:cY2AIrMgHm6oOHmR7jY+9TtjzSjQ3iG7tURJG3Y6XH0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
@@ -183,8 +184,9 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
@@ -202,6 +204,10 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-github/v50 v50.2.0 h1:j2FyongEHlO9nxXLc+LP3wuBSVU9mVxfpdYUexMpIfk=
+github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q=
+github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -225,12 +231,18 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
+github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
+github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
@@ -295,6 +307,8 @@ github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
+github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.2 h1:hAHbPm5IJGijwng3PWk09JkG9WeqChjprR5s9bBZ+OM=
github.com/matttproud/golang_protobuf_extensions v1.0.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
@@ -317,10 +331,10 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
-github.com/onsi/ginkgo/v2 v2.6.0 h1:9t9b9vRUbFq3C4qKFCGkVuq/fIHji802N1nrtkh1mNc=
-github.com/onsi/ginkgo/v2 v2.6.0/go.mod h1:63DOGlLAH8+REH8jUGdL3YpCpu7JODesutUjdENfUAc=
-github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E=
-github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM=
+github.com/onsi/ginkgo/v2 v2.6.1 h1:1xQPCjcqYw/J5LchOcp4/2q/jzJFjiAOc25chhnDw+Q=
+github.com/onsi/ginkgo/v2 v2.6.1/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo=
+github.com/onsi/gomega v1.24.2 h1:J/tulyYK6JwBldPViHJReihxxZ+22FHs0piGjQAvoUE=
+github.com/onsi/gomega v1.24.2/go.mod h1:gs3J10IS7Z7r7eXRoNJIrNqU4ToQukCJhFtKrWgHWnk=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
@@ -406,6 +420,8 @@ github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNG
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
+github.com/xanzy/go-gitlab v0.81.0 h1:ofbhZ5ZY9AjHATWQie4qd2JfncdUmvcSA/zfQB767Dk=
+github.com/xanzy/go-gitlab v0.81.0/go.mod h1:VMbY3JIWdZ/ckvHbQqkyd3iYk2aViKrNIQ23IbFMQDo=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
@@ -451,8 +467,9 @@ golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
+golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -529,8 +546,8 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
-golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 h1:Frnccbp+ok2GkUS2tC84yAq/U9Vg+0sIO7aRL3T4Xnc=
-golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -542,8 +559,8 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk=
-golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
+golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
+golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -613,14 +630,14 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
-golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
-golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
-golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
+golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -631,8 +648,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
-golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -787,8 +804,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
-google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0=
+google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -825,16 +842,16 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-k8s.io/api v0.26.0 h1:IpPlZnxBpV1xl7TGk/X6lFtpgjgntCg8PJ+qrPHAC7I=
-k8s.io/api v0.26.0/go.mod h1:k6HDTaIFC8yn1i6pSClSqIwLABIcLV9l5Q4EcngKnQg=
-k8s.io/apiextensions-apiserver v0.26.0 h1:Gy93Xo1eg2ZIkNX/8vy5xviVSxwQulsnUdQ00nEdpDo=
-k8s.io/apiextensions-apiserver v0.26.0/go.mod h1:7ez0LTiyW5nq3vADtK6C3kMESxadD51Bh6uz3JOlqWQ=
-k8s.io/apimachinery v0.26.0 h1:1feANjElT7MvPqp0JT6F3Ss6TWDwmcjLypwoPpEf7zg=
-k8s.io/apimachinery v0.26.0/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74=
-k8s.io/client-go v0.26.0 h1:lT1D3OfO+wIi9UFolCrifbjUUgu7CpLca0AD8ghRLI8=
-k8s.io/client-go v0.26.0/go.mod h1:I2Sh57A79EQsDmn7F7ASpmru1cceh3ocVT9KlX2jEZg=
-k8s.io/component-base v0.26.0 h1:0IkChOCohtDHttmKuz+EP3j3+qKmV55rM9gIFTXA7Vs=
-k8s.io/component-base v0.26.0/go.mod h1:lqHwlfV1/haa14F/Z5Zizk5QmzaVf23nQzCwVOQpfC8=
+k8s.io/api v0.26.1 h1:f+SWYiPd/GsiWwVRz+NbFyCgvv75Pk9NK6dlkZgpCRQ=
+k8s.io/api v0.26.1/go.mod h1:xd/GBNgR0f707+ATNyPmQ1oyKSgndzXij81FzWGsejg=
+k8s.io/apiextensions-apiserver v0.26.1 h1:cB8h1SRk6e/+i3NOrQgSFij1B2S0Y0wDoNl66bn8RMI=
+k8s.io/apiextensions-apiserver v0.26.1/go.mod h1:AptjOSXDGuE0JICx/Em15PaoO7buLwTs0dGleIHixSM=
+k8s.io/apimachinery v0.26.1 h1:8EZ/eGJL+hY/MYCNwhmDzVqq2lPl3N3Bo8rvweJwXUQ=
+k8s.io/apimachinery v0.26.1/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74=
+k8s.io/client-go v0.26.1 h1:87CXzYJnAMGaa/IDDfRdhTzxk/wzGZ+/HUQpqgVSZXU=
+k8s.io/client-go v0.26.1/go.mod h1:IWNSglg+rQ3OcvDkhY6+QLeasV4OYHDjdqeWkDQZwGE=
+k8s.io/component-base v0.26.1 h1:4ahudpeQXHZL5kko+iDHqLj/FSGAEUnSVO0EBbgDd+4=
+k8s.io/component-base v0.26.1/go.mod h1:VHrLR0b58oC035w6YQiBSbtsf0ThuSwXP+p5dD/kAWU=
k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4=
k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E=
diff --git a/internal/annotations/annotations.go b/internal/annotations/annotations.go
index f90a5b09e..0fb230e02 100644
--- a/internal/annotations/annotations.go
+++ b/internal/annotations/annotations.go
@@ -3,8 +3,6 @@ package annotations
import (
"context"
- configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1"
-
"sigs.k8s.io/controller-runtime/pkg/client"
)
@@ -22,22 +20,31 @@ const (
LastRelevantCommit string = "webhook.terraform.padok.cloud/relevant-commit"
ForceApply string = "notifications.terraform.padok.cloud/force-apply"
+
+ LastDiscoveredCommit string = "pullrequest.terraform.padok.cloud/last-discovered-commit"
+ LastCommentedCommit string = "pullrequest.terraform.padok.cloud/last-commented-commit"
)
-func Add(ctx context.Context, c client.Client, obj configv1alpha1.TerraformLayer, annotations map[string]string) error {
- patch := client.MergeFrom(obj.DeepCopy())
+func Add(ctx context.Context, c client.Client, obj client.Object, annotations map[string]string) error {
+ newObj := obj.DeepCopyObject().(client.Object)
+ patch := client.MergeFrom(newObj)
currentAnnotations := obj.GetAnnotations()
+ if currentAnnotations == nil {
+ currentAnnotations = make(map[string]string)
+ }
for k, v := range annotations {
currentAnnotations[k] = v
}
+
obj.SetAnnotations(currentAnnotations)
- return c.Patch(ctx, &obj, patch)
+ return c.Patch(ctx, obj, patch)
}
-func Remove(ctx context.Context, c client.Client, obj configv1alpha1.TerraformLayer, annotation string) error {
- patch := client.MergeFrom(obj.DeepCopy())
+func Remove(ctx context.Context, c client.Client, obj client.Object, annotation string) error {
+ newObj := obj.DeepCopyObject().(client.Object)
+ patch := client.MergeFrom(newObj)
annotations := obj.GetAnnotations()
delete(annotations, annotation)
obj.SetAnnotations(annotations)
- return c.Patch(ctx, &obj, patch)
+ return c.Patch(ctx, obj, patch)
}
diff --git a/internal/burrito/config/config.go b/internal/burrito/config/config.go
index d715b1bca..033e000de 100644
--- a/internal/burrito/config/config.go
+++ b/internal/burrito/config/config.go
@@ -29,7 +29,9 @@ type WebhookGithubConfig struct {
}
type WebhookGitlabConfig struct {
- Secret string `yaml:"secret"`
+ URL string `yaml:"url"`
+ Secret string `yaml:"secret"`
+ APIToken string `yaml:"token"`
}
type ControllerConfig struct {
@@ -40,6 +42,17 @@ type ControllerConfig struct {
MetricsBindAddress string `yaml:"metricsBindAddress"`
HealthProbeBindAddress string `yaml:"healthProbeBindAddress"`
KubernetesWehbookPort int `yaml:"kubernetesWebhookPort"`
+ GithubConfig GithubConfig `yaml:"githubConfig"`
+ GitlabConfig GitlabConfig `yaml:"gitlabConfig"`
+}
+
+type GithubConfig struct {
+ APIToken string `yaml:"apiToken"`
+}
+
+type GitlabConfig struct {
+ APIToken string `yaml:"token"`
+ URL string `yaml:"url"`
}
type LeaderElectionConfig struct {
@@ -49,8 +62,8 @@ type LeaderElectionConfig struct {
type ControllerTimers struct {
DriftDetection time.Duration `yaml:"driftDetection"`
- OnError time.Duration `yaml:"waitAction"`
- WaitAction time.Duration `yaml:"onError"`
+ OnError time.Duration `yaml:"onError"`
+ WaitAction time.Duration `yaml:"waitAction"`
}
type RepositoryConfig struct {
diff --git a/internal/controllers/manager.go b/internal/controllers/manager.go
index d181a6f8f..6b0e90cae 100644
--- a/internal/controllers/manager.go
+++ b/internal/controllers/manager.go
@@ -30,6 +30,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/padok-team/burrito/internal/controllers/terraformlayer"
+ "github.com/padok-team/burrito/internal/controllers/terraformpullrequest"
"github.com/padok-team/burrito/internal/controllers/terraformrepository"
"github.com/padok-team/burrito/internal/storage/redis"
@@ -99,6 +100,15 @@ func (c *Controllers) Exec() {
log.Fatalf("unable to create repository controller: %s", err)
}
log.Infof("repository controller started successfully")
+ case "pullrequest":
+ if err = (&terraformpullrequest.Reconciler{
+ Client: mgr.GetClient(),
+ Scheme: mgr.GetScheme(),
+ Config: c.config,
+ }).SetupWithManager(mgr); err != nil {
+ log.Fatalf("unable to create pullrequest controller: %s", err)
+ }
+ log.Infof("pullrequest controller started successfully")
default:
log.Infof("unrecognized controller type %s, ignoring", ctrlType)
}
diff --git a/internal/controllers/terraformlayer/conditions.go b/internal/controllers/terraformlayer/conditions.go
index 8f7ee4e64..ff8530ff4 100644
--- a/internal/controllers/terraformlayer/conditions.go
+++ b/internal/controllers/terraformlayer/conditions.go
@@ -72,13 +72,7 @@ func (r *Reconciler) IsLastRelevantCommitPlanned(t *configv1alpha1.TerraformLaye
condition.Status = metav1.ConditionTrue
return condition, true
}
- if lastBranchCommit != lastRelevantCommit {
- condition.Reason = "CommitAlreadyHadnled"
- condition.Message = "The last relevant commit should already have been planned"
- condition.Status = metav1.ConditionTrue
- return condition, true
- }
- if lastPlannedCommit == lastBranchCommit {
+ if lastPlannedCommit == lastBranchCommit || lastPlannedCommit == lastRelevantCommit {
condition.Reason = "LastRelevantCommitPlanned"
condition.Message = "The last relevant commit has already been planned"
condition.Status = metav1.ConditionTrue
diff --git a/internal/controllers/terraformlayer/controller.go b/internal/controllers/terraformlayer/controller.go
index 67baa0787..639c02918 100644
--- a/internal/controllers/terraformlayer/controller.go
+++ b/internal/controllers/terraformlayer/controller.go
@@ -27,6 +27,8 @@ import (
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+ "sigs.k8s.io/controller-runtime/pkg/predicate"
log "github.com/sirupsen/logrus"
@@ -109,5 +111,19 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&configv1alpha1.TerraformLayer{}).
+ WithEventFilter(ignorePredicate()).
Complete(r)
}
+
+func ignorePredicate() predicate.Predicate {
+ return predicate.Funcs{
+ UpdateFunc: func(e event.UpdateEvent) bool {
+ // Ignore updates to CR status in which case metadata.Generation does not change
+ return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration()
+ },
+ DeleteFunc: func(e event.DeleteEvent) bool {
+ // Evaluates to false if the object has been confirmed deleted.
+ return !e.DeleteStateUnknown
+ },
+ }
+}
diff --git a/internal/controllers/terraformlayer/pod.go b/internal/controllers/terraformlayer/pod.go
index 1d9cc5704..b87f2a8e7 100644
--- a/internal/controllers/terraformlayer/pod.go
+++ b/internal/controllers/terraformlayer/pod.go
@@ -72,6 +72,7 @@ func (r *Reconciler) getPod(layer *configv1alpha1.TerraformLayer, repository *co
}
overrideSpec := configv1alpha1.GetOverrideRunnerSpec(repository, layer)
+
defaultSpec.Tolerations = overrideSpec.Tolerations
defaultSpec.NodeSelector = overrideSpec.NodeSelector
defaultSpec.Containers[0].Env = append(defaultSpec.Containers[0].Env, overrideSpec.Env...)
@@ -79,10 +80,15 @@ func (r *Reconciler) getPod(layer *configv1alpha1.TerraformLayer, repository *co
defaultSpec.Containers[0].VolumeMounts = append(defaultSpec.Containers[0].VolumeMounts, overrideSpec.VolumeMounts...)
defaultSpec.Containers[0].Resources = overrideSpec.Resources
defaultSpec.Containers[0].EnvFrom = append(defaultSpec.Containers[0].EnvFrom, overrideSpec.EnvFrom...)
- defaultSpec.Containers[0].Image = overrideSpec.Image
- defaultSpec.ServiceAccountName = overrideSpec.ServiceAccountName
defaultSpec.ImagePullSecrets = append(defaultSpec.ImagePullSecrets, overrideSpec.ImagePullSecrets...)
+ if len(overrideSpec.ServiceAccountName) > 0 {
+ defaultSpec.ServiceAccountName = overrideSpec.ServiceAccountName
+ }
+ if len(overrideSpec.Image) > 0 {
+ defaultSpec.Containers[0].Image = overrideSpec.Image
+ }
+
pod := corev1.Pod{
Spec: defaultSpec,
ObjectMeta: metav1.ObjectMeta{
diff --git a/internal/controllers/terraformlayer/states.go b/internal/controllers/terraformlayer/states.go
index 0242ae003..e94df8c6c 100644
--- a/internal/controllers/terraformlayer/states.go
+++ b/internal/controllers/terraformlayer/states.go
@@ -12,8 +12,10 @@ import (
ctrl "sigs.k8s.io/controller-runtime"
)
+type Handler func(context.Context, *Reconciler, *configv1alpha1.TerraformLayer, *configv1alpha1.TerraformRepository) ctrl.Result
+
type State interface {
- getHandler() func(ctx context.Context, r *Reconciler, layer *configv1alpha1.TerraformLayer, repository *configv1alpha1.TerraformRepository) ctrl.Result
+ getHandler() Handler
}
func (r *Reconciler) GetState(ctx context.Context, layer *configv1alpha1.TerraformLayer) (State, []metav1.Condition) {
@@ -24,7 +26,7 @@ func (r *Reconciler) GetState(ctx context.Context, layer *configv1alpha1.Terrafo
// c3, hasFailed := HasFailed(r)
conditions := []metav1.Condition{c1, c2, c3}
switch {
- case isPlanArtifactUpToDate && isApplyUpToDate:
+ case isPlanArtifactUpToDate && isApplyUpToDate && isLastRelevantCommitPlanned:
log.Infof("layer %s is up to date, waiting for a new drift detection cycle", layer.Name)
return &Idle{}, conditions
case !isPlanArtifactUpToDate || !isLastRelevantCommitPlanned:
@@ -41,7 +43,7 @@ func (r *Reconciler) GetState(ctx context.Context, layer *configv1alpha1.Terrafo
type Idle struct{}
-func (s *Idle) getHandler() func(ctx context.Context, r *Reconciler, layer *configv1alpha1.TerraformLayer, repository *configv1alpha1.TerraformRepository) ctrl.Result {
+func (s *Idle) getHandler() Handler {
return func(ctx context.Context, r *Reconciler, layer *configv1alpha1.TerraformLayer, repository *configv1alpha1.TerraformRepository) ctrl.Result {
return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.DriftDetection}
}
@@ -49,7 +51,7 @@ func (s *Idle) getHandler() func(ctx context.Context, r *Reconciler, layer *conf
type PlanNeeded struct{}
-func (s *PlanNeeded) getHandler() func(ctx context.Context, r *Reconciler, layer *configv1alpha1.TerraformLayer, repository *configv1alpha1.TerraformRepository) ctrl.Result {
+func (s *PlanNeeded) getHandler() Handler {
return func(ctx context.Context, r *Reconciler, layer *configv1alpha1.TerraformLayer, repository *configv1alpha1.TerraformRepository) ctrl.Result {
log := log.WithContext(ctx)
err := lock.CreateLock(ctx, r.Client, layer)
@@ -70,7 +72,7 @@ func (s *PlanNeeded) getHandler() func(ctx context.Context, r *Reconciler, layer
type ApplyNeeded struct{}
-func (s *ApplyNeeded) getHandler() func(ctx context.Context, r *Reconciler, layer *configv1alpha1.TerraformLayer, repository *configv1alpha1.TerraformRepository) ctrl.Result {
+func (s *ApplyNeeded) getHandler() Handler {
return func(ctx context.Context, r *Reconciler, layer *configv1alpha1.TerraformLayer, repository *configv1alpha1.TerraformRepository) ctrl.Result {
log := log.WithContext(ctx)
remediationStrategy := getRemediationStrategy(repository, layer)
diff --git a/internal/controllers/terraformpullrequest/comment/common.go b/internal/controllers/terraformpullrequest/comment/common.go
new file mode 100644
index 000000000..4478ffd33
--- /dev/null
+++ b/internal/controllers/terraformpullrequest/comment/common.go
@@ -0,0 +1,5 @@
+package comment
+
+type Comment interface {
+ Generate(string) (string, error)
+}
diff --git a/internal/controllers/terraformpullrequest/comment/default.go b/internal/controllers/terraformpullrequest/comment/default.go
new file mode 100644
index 000000000..c8123ecba
--- /dev/null
+++ b/internal/controllers/terraformpullrequest/comment/default.go
@@ -0,0 +1,71 @@
+package comment
+
+import (
+ "bytes"
+ "text/template"
+
+ configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1"
+ "github.com/padok-team/burrito/internal/storage"
+
+ _ "embed"
+)
+
+var (
+ //go:embed templates/comment.md
+ defaultTemplateRaw string
+ defaultTemplate = template.Must(template.New("report").Parse(defaultTemplateRaw))
+)
+
+type ReportedLayer struct {
+ ShortDiff string
+ Path string
+ PrettyPlan string
+}
+
+type DefaultComment struct {
+ layers []configv1alpha1.TerraformLayer
+ storage storage.Storage
+}
+
+type DefaultCommentInput struct {
+}
+
+func NewDefaultComment(layers []configv1alpha1.TerraformLayer, storage storage.Storage) *DefaultComment {
+ return &DefaultComment{
+ layers: layers,
+ storage: storage,
+ }
+}
+
+func (c *DefaultComment) Generate(commit string) (string, error) {
+ var reportedLayers []ReportedLayer
+ for _, layer := range c.layers {
+ prettyPlanKey := storage.GenerateKey(storage.LastPrettyPlan, &layer)
+ plan, err := c.storage.Get(prettyPlanKey)
+ if err != nil {
+ return "", err
+ }
+ shortDiffKey := storage.GenerateKey(storage.LastPlanResult, &layer)
+ shortDiff, err := c.storage.Get(shortDiffKey)
+ if err != nil {
+ return "", err
+ }
+ reportedLayer := ReportedLayer{
+ Path: layer.Spec.Path,
+ ShortDiff: string(shortDiff),
+ PrettyPlan: string(plan),
+ }
+ reportedLayers = append(reportedLayers, reportedLayer)
+
+ }
+ data := struct {
+ Commit string
+ Layers []ReportedLayer
+ }{
+ Commit: commit,
+ Layers: reportedLayers,
+ }
+ comment := bytes.NewBufferString("")
+ defaultTemplate.Execute(comment, data)
+ return comment.String(), nil
+}
diff --git a/internal/controllers/terraformpullrequest/comment/default_test.go b/internal/controllers/terraformpullrequest/comment/default_test.go
new file mode 100644
index 000000000..3bce9fc33
--- /dev/null
+++ b/internal/controllers/terraformpullrequest/comment/default_test.go
@@ -0,0 +1,36 @@
+package comment
+
+import (
+ _ "embed"
+)
+
+// func TestDefaultComment_Generate(t *testing.T) {
+// type fields struct {
+// layers []configv1alpha1.TerraformLayer
+// storage storage.Storage
+// }
+// tests := []struct {
+// name string
+// fields fields
+// want string
+// wantErr bool
+// }{
+// // TODO: Add test cases.
+// }
+// for _, tt := range tests {
+// t.Run(tt.name, func(t *testing.T) {
+// c := &DefaultComment{
+// layers: tt.fields.layers,
+// storage: tt.fields.storage,
+// }
+// got, err := c.Generate()
+// if (err != nil) != tt.wantErr {
+// t.Errorf("DefaultComment.Generate() error = %v, wantErr %v", err, tt.wantErr)
+// return
+// }
+// if got != tt.want {
+// t.Errorf("DefaultComment.Generate() = %v, want %v", got, tt.want)
+// }
+// })
+// }
+// }
diff --git a/internal/controllers/terraformpullrequest/comment/initial.go b/internal/controllers/terraformpullrequest/comment/initial.go
new file mode 100644
index 000000000..4677840ab
--- /dev/null
+++ b/internal/controllers/terraformpullrequest/comment/initial.go
@@ -0,0 +1,8 @@
+package comment
+
+type InitialComment struct {
+}
+
+func NewInitialComment() *InitialComment {
+ return &InitialComment{}
+}
diff --git a/internal/controllers/terraformpullrequest/comment/templates/comment.md b/internal/controllers/terraformpullrequest/comment/templates/comment.md
new file mode 100644
index 000000000..f2032438c
--- /dev/null
+++ b/internal/controllers/terraformpullrequest/comment/templates/comment.md
@@ -0,0 +1,17 @@
+## :burrito: Burrito Report
+
+{{ len .Layers }} layer(s) affected with {{ .Commit }} commit.
+
+{{- range .Layers }}
+### Layer {{ .Path }}
+
+`{{ .ShortDiff }}`
+
+
+Plan
+
+```
+{{ .PrettyPlan }}
+```
+
+{{- end }}
diff --git a/internal/controllers/terraformpullrequest/conditions.go b/internal/controllers/terraformpullrequest/conditions.go
new file mode 100644
index 000000000..5b83cd6af
--- /dev/null
+++ b/internal/controllers/terraformpullrequest/conditions.go
@@ -0,0 +1,159 @@
+package terraformpullrequest
+
+import (
+ "context"
+ "time"
+
+ configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1"
+ "github.com/padok-team/burrito/internal/annotations"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/labels"
+ "k8s.io/apimachinery/pkg/selection"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+func (r *Reconciler) IsLastCommitDiscovered(pr *configv1alpha1.TerraformPullRequest) (metav1.Condition, bool) {
+ condition := metav1.Condition{
+ Type: "IsLastCommitDiscovered",
+ ObservedGeneration: pr.GetObjectMeta().GetGeneration(),
+ Status: metav1.ConditionUnknown,
+ LastTransitionTime: metav1.NewTime(time.Now()),
+ }
+ lastDiscorveredCommit, ok := pr.Annotations[annotations.LastDiscoveredCommit]
+ if !ok {
+ condition.Reason = "NoCommitDiscovered"
+ condition.Message = "Controller hasn't discovered any commit yet."
+ condition.Status = metav1.ConditionFalse
+ return condition, false
+ }
+ lastBranchCommit, ok := pr.Annotations[annotations.LastBranchCommit]
+ if !ok {
+ condition.Reason = "UnknownLastBranchCommit"
+ condition.Message = "This should have happened"
+ condition.Status = metav1.ConditionFalse
+ return condition, false
+ }
+ if lastDiscorveredCommit == lastBranchCommit {
+ condition.Reason = "LastCommitDiscovered"
+ condition.Message = "The last commit has been discovered."
+ condition.Status = metav1.ConditionTrue
+ return condition, true
+ }
+ condition.Reason = "LastCommitNotDiscovered"
+ condition.Message = "Last received commit is not the last discovered commit."
+ condition.Status = metav1.ConditionFalse
+ return condition, false
+}
+
+func (r *Reconciler) AreLayersStillPlanning(pr *configv1alpha1.TerraformPullRequest) (metav1.Condition, bool) {
+ condition := metav1.Condition{
+ Type: "AreLayersStillPlanning",
+ ObservedGeneration: pr.GetObjectMeta().GetGeneration(),
+ Status: metav1.ConditionUnknown,
+ LastTransitionTime: metav1.NewTime(time.Now()),
+ }
+ layers, err := getLinkedLayers(r.Client, pr)
+
+ lastDiscoveredCommit, okDiscoveredCommit := pr.Annotations[annotations.LastDiscoveredCommit]
+ prLastBranchCommit, okPRBranchCommit := pr.Annotations[annotations.LastBranchCommit]
+
+ if !okPRBranchCommit {
+ condition.Reason = "NoBranchCommitOnPR"
+ condition.Message = "This should not have happened, report this as an issue"
+ condition.Status = metav1.ConditionUnknown
+ return condition, true
+ }
+
+ if !okDiscoveredCommit {
+ condition.Reason = "NoCommitDiscovered"
+ condition.Message = "Controller hasn't discovered any commit yet."
+ condition.Status = metav1.ConditionTrue
+ return condition, true
+ }
+
+ if lastDiscoveredCommit != prLastBranchCommit {
+ condition.Reason = "StillNeedsDiscovery"
+ condition.Message = "Controller hasn't discovered the latest commit yet."
+ condition.Status = metav1.ConditionTrue
+ return condition, true
+ }
+
+ if err != nil {
+ condition.Reason = "ErrorListingLayers"
+ condition.Message = err.Error()
+ condition.Status = metav1.ConditionTrue
+ return condition, true
+ }
+
+ for _, layer := range layers {
+ lastRelevantCommit, okRelevantCommit := layer.Annotations[annotations.LastRelevantCommit]
+ lastPlanCommit, okPlanCommit := layer.Annotations[annotations.LastPlanCommit]
+ condition.Reason = "LayersStillPlanning"
+ condition.Message = "Linked layers are still planning."
+ condition.Status = metav1.ConditionTrue
+ if !okPlanCommit {
+ return condition, true
+ }
+ if !okRelevantCommit {
+ condition.Reason = "NoRelevantCommitOnLayer"
+ condition.Message = "This should not have happened, report this as an issue"
+ condition.Status = metav1.ConditionUnknown
+ return condition, true
+ }
+ if lastPlanCommit == lastRelevantCommit {
+ continue
+ }
+ return condition, true
+ }
+ condition.Reason = "LayersNotPlanning"
+ condition.Message = "Linked layers are not planning."
+ condition.Status = metav1.ConditionFalse
+ return condition, false
+}
+
+func (r *Reconciler) IsCommentUpToDate(pr *configv1alpha1.TerraformPullRequest) (metav1.Condition, bool) {
+ condition := metav1.Condition{
+ Type: "IsCommentUpToDate",
+ ObservedGeneration: pr.GetObjectMeta().GetGeneration(),
+ Status: metav1.ConditionUnknown,
+ LastTransitionTime: metav1.NewTime(time.Now()),
+ }
+ lasCommentedCommit, ok := pr.Annotations[annotations.LastCommentedCommit]
+ if !ok {
+ condition.Reason = "NoCommentSent"
+ condition.Message = "No comment has ever been sent"
+ condition.Status = metav1.ConditionFalse
+ return condition, false
+ }
+ lastDiscorveredCommit, ok := pr.Annotations[annotations.LastDiscoveredCommit]
+ if !ok {
+ condition.Reason = "Unknown"
+ condition.Message = "This should not have happened"
+ condition.Status = metav1.ConditionUnknown
+ return condition, true
+ }
+ if lasCommentedCommit != lastDiscorveredCommit {
+ condition.Reason = "CommentOutdated"
+ condition.Message = "The comment is outdated."
+ condition.Status = metav1.ConditionFalse
+ return condition, false
+ }
+ condition.Reason = "CommentUpToDate"
+ condition.Message = "The comment is up to date."
+ condition.Status = metav1.ConditionTrue
+ return condition, true
+}
+
+func getLinkedLayers(cl client.Client, pr *configv1alpha1.TerraformPullRequest) ([]configv1alpha1.TerraformLayer, error) {
+ layers := configv1alpha1.TerraformLayerList{}
+ requirement, err := labels.NewRequirement("burrito/managed-by", selection.Equals, []string{pr.Name})
+ if err != nil {
+ return nil, err
+ }
+ selector := labels.NewSelector().Add(*requirement)
+ err = cl.List(context.TODO(), &layers, client.MatchingLabelsSelector{Selector: selector})
+ if err != nil {
+ return nil, err
+ }
+ return layers.Items, nil
+}
diff --git a/internal/controllers/terraformpullrequest/controller.go b/internal/controllers/terraformpullrequest/controller.go
new file mode 100644
index 000000000..25ad8139a
--- /dev/null
+++ b/internal/controllers/terraformpullrequest/controller.go
@@ -0,0 +1,128 @@
+package terraformpullrequest
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "github.com/padok-team/burrito/internal/burrito/config"
+ "github.com/padok-team/burrito/internal/controllers/terraformpullrequest/comment"
+ "github.com/padok-team/burrito/internal/controllers/terraformpullrequest/github"
+ "github.com/padok-team/burrito/internal/storage"
+ "github.com/padok-team/burrito/internal/storage/redis"
+
+ "github.com/padok-team/burrito/internal/controllers/terraformpullrequest/gitlab"
+ "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+ "sigs.k8s.io/controller-runtime/pkg/predicate"
+
+ log "github.com/sirupsen/logrus"
+
+ configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1"
+)
+
+type Provider interface {
+ Init(*config.Config) error
+ IsFromProvider(*configv1alpha1.TerraformPullRequest) bool
+ GetChanges(*configv1alpha1.TerraformRepository, *configv1alpha1.TerraformPullRequest) ([]string, error)
+ Comment(*configv1alpha1.TerraformRepository, *configv1alpha1.TerraformPullRequest, comment.Comment) error
+}
+
+// Reconciler reconciles a TerraformPullRequest object
+type Reconciler struct {
+ client.Client
+ Scheme *runtime.Scheme
+ Config *config.Config
+ Providers []Provider
+ Storage storage.Storage
+}
+
+//+kubebuilder:rbac:groups=config.terraform.padok.cloud,resources=terraformpullrequests,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=config.terraform.padok.cloud,resources=terraformpullrequests/status,verbs=get;update;patch
+//+kubebuilder:rbac:groups=config.terraform.padok.cloud,resources=terraformpullrequests/finalizers,verbs=update
+
+// Reconcile is part of the main kubernetes reconciliation loop which aims to
+// move the current state of the cluster closer to the desired state.
+// TODO(user): Modify the Reconcile function to compare the state specified by
+// the TerraformLayer object against the actual cluster state, and then
+// perform operations to make the cluster state reflect the state specified by
+// the user.
+//
+// For more details, check Reconcile and its Result here:
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.0/pkg/reconcile
+func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+
+ log := log.WithContext(ctx)
+ log.Infof("starting reconciliation...")
+ pr := &configv1alpha1.TerraformPullRequest{}
+ err := r.Client.Get(ctx, req.NamespacedName, pr)
+ if errors.IsNotFound(err) {
+ log.Errorf("resource not found. Ignoring since object must be deleted: %s", err)
+ return ctrl.Result{}, nil
+ }
+ if err != nil {
+ log.Errorf("failed to get TerraformPullRequest: %s", err)
+ return ctrl.Result{}, err
+ }
+ repository := &configv1alpha1.TerraformRepository{}
+ err = r.Client.Get(ctx, types.NamespacedName{
+ Name: pr.Spec.Repository.Name,
+ Namespace: pr.Spec.Repository.Namespace,
+ }, repository)
+ if errors.IsNotFound(err) {
+ log.Errorf("repository not found. object must not be configured correctly: %s", err)
+ return ctrl.Result{}, nil
+ }
+ if err != nil {
+ log.Errorf("failed to get TerraformRepository: %s", err)
+ return ctrl.Result{}, err
+ }
+ state, conditions := r.GetState(ctx, pr)
+ pr.Status = configv1alpha1.TerraformPullRequestStatus{Conditions: conditions, State: getStateString(state)}
+
+ result := state.getHandler()(ctx, r, repository, pr)
+ err = r.Client.Status().Update(ctx, pr)
+ if err != nil {
+ log.Errorf("could not update pull request %s status: %s", pr.Name, err)
+ }
+ log.Infof("finished reconciliation cycle for pull request %s", pr.Name)
+ return result, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
+ providers := []Provider{}
+ for _, p := range []Provider{&github.Github{}, &gitlab.Gitlab{}} {
+ name := strings.Split(fmt.Sprintf("%T", p), ".")
+ err := p.Init(r.Config)
+ if err != nil {
+ log.Warnf("could not initialize provider %s: %s", name, err)
+ continue
+ }
+ log.Infof("provider %s successfully initialized", name)
+ providers = append(providers, p)
+ }
+ r.Providers = providers
+ r.Storage = redis.New(r.Config.Redis.URL, r.Config.Redis.Password, r.Config.Redis.Database)
+ return ctrl.NewControllerManagedBy(mgr).
+ For(&configv1alpha1.TerraformPullRequest{}).
+ WithEventFilter(ignorePredicate()).
+ Complete(r)
+}
+
+func ignorePredicate() predicate.Predicate {
+ return predicate.Funcs{
+ UpdateFunc: func(e event.UpdateEvent) bool {
+ // Ignore updates to CR status in which case metadata.Generation does not change
+ return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration()
+ },
+ DeleteFunc: func(e event.DeleteEvent) bool {
+ // Evaluates to false if the object has been confirmed deleted.
+ return !e.DeleteStateUnknown
+ },
+ }
+}
diff --git a/internal/controllers/terraformpullrequest/github/provider.go b/internal/controllers/terraformpullrequest/github/provider.go
new file mode 100644
index 000000000..f4e4cfe45
--- /dev/null
+++ b/internal/controllers/terraformpullrequest/github/provider.go
@@ -0,0 +1,118 @@
+package github
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/google/go-github/v50/github"
+ configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1"
+ "github.com/padok-team/burrito/internal/annotations"
+ "github.com/padok-team/burrito/internal/burrito/config"
+ "github.com/padok-team/burrito/internal/controllers/terraformpullrequest/comment"
+ log "github.com/sirupsen/logrus"
+ "golang.org/x/oauth2"
+)
+
+type Github struct {
+ *github.Client
+}
+
+func (g *Github) IsConfigPresent(c *config.Config) bool {
+ if &c.Controller.GithubConfig == nil {
+ return false
+ }
+ if c.Controller.GithubConfig.APIToken == "" {
+ return false
+ }
+ return true
+}
+
+func (g *Github) Init(c *config.Config) error {
+ ctx := context.Background()
+ if !g.IsConfigPresent(c) {
+ return errors.New("github config is not present")
+ }
+ ts := oauth2.StaticTokenSource(
+ &oauth2.Token{AccessToken: c.Controller.GithubConfig.APIToken},
+ )
+ tc := oauth2.NewClient(ctx, ts)
+
+ g.Client = github.NewClient(tc)
+ return nil
+}
+
+func (g *Github) IsFromProvider(pr *configv1alpha1.TerraformPullRequest) bool {
+ return pr.Spec.Provider == "github"
+}
+
+func (g *Github) GetChanges(repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest) ([]string, error) {
+ owner, repoName := parseGithubUrl(repository.Spec.Repository.Url)
+ id, err := strconv.Atoi(pr.Spec.ID)
+ if err != nil {
+ log.Errorf("Error while parsing Github pull request ID: %s", err)
+ return []string{}, err
+ }
+ // Per page is 30 by default, max is 100
+ opts := &github.RepositoryListByOrgOptions{
+ ListOptions: github.ListOptions{PerPage: 100},
+ }
+ // Get all pull request files from Github
+ var allChangedFiles []string
+ for {
+ changedFiles, resp, err := g.Client.PullRequests.ListFiles(context.TODO(), owner, repoName, id, nil)
+ if err != nil {
+ return []string{}, err
+ }
+ for _, file := range changedFiles {
+ if *file.Status != "unchanged" {
+ allChangedFiles = append(allChangedFiles, *file.Filename)
+ }
+ }
+ if resp.NextPage == 0 {
+ break
+ }
+ opts.Page = resp.NextPage
+ }
+ return allChangedFiles, nil
+}
+
+func (g *Github) Comment(repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest, comment comment.Comment) error {
+ body, err := comment.Generate(pr.Annotations[annotations.LastBranchCommit])
+ if err != nil {
+ log.Errorf("Error while generating comment: %s", err)
+ return err
+ }
+ owner, repoName := parseGithubUrl(repository.Spec.Repository.Url)
+ id, err := strconv.Atoi(pr.Spec.ID)
+ if err != nil {
+ log.Errorf("Error while parsing Github pull request ID: %s", err)
+ return err
+ }
+ _, _, err = g.Client.Issues.CreateComment(context.TODO(), owner, repoName, id, &github.IssueComment{
+ Body: &body,
+ })
+ return err
+}
+
+func parseGithubUrl(url string) (string, string) {
+ normalizedUrl := normalizeUrl(url)
+ // nomalized url are "https://padok.github.com/owner/repo"
+ // we remove "https://" then split on "/"
+ split := strings.Split(normalizedUrl[8:], "/")
+ return split[1], split[2]
+}
+
+func normalizeUrl(url string) string {
+ if strings.Contains(url, "https://") {
+ return url
+ }
+ // All SSH URL from GitHub are like "git@padok.github.com:/.git"
+ // We split on ":" then remove ".git" by removing the last characters
+ // To handle enterprise GitHub, we dynamically get "padok.github.com"
+ // By removing "git@" at the beginning of the string
+ split := strings.Split(url, ":")
+ return fmt.Sprintf("https://%s/%s", split[0][4:], split[1][:len(split[1])-4])
+}
diff --git a/internal/controllers/terraformpullrequest/gitlab/provider.go b/internal/controllers/terraformpullrequest/gitlab/provider.go
new file mode 100644
index 000000000..01f1d8df6
--- /dev/null
+++ b/internal/controllers/terraformpullrequest/gitlab/provider.go
@@ -0,0 +1,104 @@
+package gitlab
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1"
+ "github.com/padok-team/burrito/internal/annotations"
+ "github.com/padok-team/burrito/internal/burrito/config"
+ "github.com/padok-team/burrito/internal/controllers/terraformpullrequest/comment"
+ log "github.com/sirupsen/logrus"
+ "github.com/xanzy/go-gitlab"
+)
+
+type Gitlab struct {
+ *gitlab.Client
+}
+
+func (g *Gitlab) IsConfigPresent(c *config.Config) bool {
+ if &c.Controller.GitlabConfig == nil {
+ return false
+ }
+ if c.Controller.GitlabConfig.APIToken == "" {
+ return false
+ }
+ return true
+}
+
+func (g *Gitlab) Init(c *config.Config) error {
+ if !g.IsConfigPresent(c) {
+ return fmt.Errorf("gitlab config is not present")
+ }
+ client, err := gitlab.NewClient(c.Controller.GitlabConfig.APIToken, gitlab.WithBaseURL(c.Controller.GitlabConfig.URL))
+ if err != nil {
+ return err
+ }
+ g.Client = client
+ return nil
+}
+
+func (g *Gitlab) IsFromProvider(pr *configv1alpha1.TerraformPullRequest) bool {
+ return pr.Spec.Provider == "gitlab"
+}
+
+func (g *Gitlab) GetChanges(repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest) ([]string, error) {
+ id, err := strconv.Atoi(pr.Spec.ID)
+ if err != nil {
+ log.Errorf("Error while parsing Gitlab merge request ID: %s", err)
+ return []string{}, err
+ }
+ getOpts := gitlab.GetMergeRequestChangesOptions{
+ AccessRawDiffs: gitlab.Bool(true),
+ }
+
+ mr, _, err := g.Client.MergeRequests.GetMergeRequestChanges(getGitlabNamespacedName(repository.Spec.Repository.Url), id, &getOpts)
+ if err != nil {
+ log.Errorf("Error while getting merge request changes: %s", err)
+ return []string{}, err
+ }
+ var changes []string
+ for _, change := range mr.Changes {
+ changes = append(changes, change.NewPath)
+ }
+ return changes, nil
+}
+
+func (g *Gitlab) Comment(repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest, comment comment.Comment) error {
+ body, err := comment.Generate(pr.Annotations[annotations.LastBranchCommit])
+ if err != nil {
+ log.Errorf("Error while generating comment: %s", err)
+ return err
+ }
+ id, err := strconv.Atoi(pr.Spec.ID)
+ if err != nil {
+ log.Errorf("Error while parsing Gitlab merge request ID: %s", err)
+ return err
+ }
+ _, _, err = g.Client.Notes.CreateMergeRequestNote(getGitlabNamespacedName(repository.Spec.Repository.Url), id, &gitlab.CreateMergeRequestNoteOptions{
+ Body: gitlab.String(body),
+ })
+ if err != nil {
+ log.Errorf("Error while creating merge request note: %s", err)
+ return err
+ }
+ return nil
+}
+
+func getGitlabNamespacedName(url string) string {
+ normalizedUrl := normalizeUrl(url)
+ return strings.Join(strings.Split(normalizedUrl[8:], "/")[1:], "/")
+}
+
+func normalizeUrl(url string) string {
+ if strings.Contains(url, "https://") {
+ return url
+ }
+ // All SSH URL from GitLab are like "git@:/.git"
+ // We split on ":" then remove ".git" by removing the last characters
+ // To handle enterprise GitLab on premise, we dynamically get "padok.gitlab.com"
+ // By removing "git@" at the beginning of the string
+ split := strings.Split(url, ":")
+ return fmt.Sprintf("https://%s/%s", split[0][4:], split[1][:len(split[1])-4])
+}
diff --git a/internal/controllers/terraformpullrequest/layer.go b/internal/controllers/terraformpullrequest/layer.go
new file mode 100644
index 000000000..1cd07fe98
--- /dev/null
+++ b/internal/controllers/terraformpullrequest/layer.go
@@ -0,0 +1,109 @@
+package terraformpullrequest
+
+import (
+ "context"
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1"
+ "github.com/padok-team/burrito/internal/annotations"
+)
+
+func (r *Reconciler) getAffectedLayers(repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest) ([]configv1alpha1.TerraformLayer, error) {
+ var layers configv1alpha1.TerraformLayerList
+ err := r.Client.List(context.Background(), &layers)
+ if err != nil {
+ return nil, err
+ }
+ var provider Provider
+ for _, p := range r.Providers {
+ if p.IsFromProvider(pr) {
+ provider = p
+ break
+ }
+ }
+ if provider == nil {
+ return nil, fmt.Errorf("could not find provider for pull request %s", pr.Name)
+ }
+ changes, err := provider.GetChanges(repository, pr)
+ if err != nil {
+ return nil, err
+ }
+ affectedLayers := []configv1alpha1.TerraformLayer{}
+ for _, layer := range layers.Items {
+ if layer.Spec.Repository != pr.Spec.Repository {
+ continue
+ }
+ if layer.Spec.Branch != pr.Spec.Base {
+ continue
+ }
+ if layerFilesHaveChanged(layer, changes) {
+ affectedLayers = append(affectedLayers, layer)
+ }
+ }
+
+ return affectedLayers, nil
+}
+
+func generateTempLayers(pr *configv1alpha1.TerraformPullRequest, layers []configv1alpha1.TerraformLayer) []configv1alpha1.TerraformLayer {
+ list := []configv1alpha1.TerraformLayer{}
+ for _, layer := range layers {
+ new := configv1alpha1.TerraformLayer{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: layer.ObjectMeta.Namespace,
+ GenerateName: fmt.Sprintf("%s-%s-", layer.Name, pr.Spec.ID),
+ Annotations: map[string]string{
+ annotations.LastBranchCommit: pr.Annotations[annotations.LastBranchCommit],
+ annotations.LastRelevantCommit: pr.Annotations[annotations.LastBranchCommit],
+ },
+ OwnerReferences: []metav1.OwnerReference{
+ {
+ APIVersion: pr.APIVersion,
+ Kind: pr.Kind,
+ Name: pr.Name,
+ UID: pr.UID,
+ },
+ },
+ Labels: map[string]string{
+ "burrito/managed-by": pr.Name,
+ },
+ },
+ Spec: configv1alpha1.TerraformLayerSpec{
+ Path: layer.Spec.Path,
+ Branch: pr.Spec.Branch,
+ TerraformConfig: layer.Spec.TerraformConfig,
+ Repository: layer.Spec.Repository,
+ RemediationStrategy: "dry",
+ OverrideRunnerSpec: layer.Spec.OverrideRunnerSpec,
+ },
+ }
+ list = append(list, new)
+ }
+ return list
+}
+
+func layerFilesHaveChanged(layer configv1alpha1.TerraformLayer, changedFiles []string) bool {
+ if len(changedFiles) == 0 {
+ return true
+ }
+
+ // At last one changed file must be under refresh path
+ for _, f := range changedFiles {
+ f = ensureAbsPath(f)
+ if strings.Contains(f, layer.Spec.Path) {
+ return true
+ }
+ }
+
+ return false
+}
+
+func ensureAbsPath(input string) string {
+ if !filepath.IsAbs(input) {
+ return string(filepath.Separator) + input
+ }
+ return input
+}
diff --git a/internal/controllers/terraformpullrequest/states.go b/internal/controllers/terraformpullrequest/states.go
new file mode 100644
index 000000000..2518155b6
--- /dev/null
+++ b/internal/controllers/terraformpullrequest/states.go
@@ -0,0 +1,129 @@
+package terraformpullrequest
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1"
+ "github.com/padok-team/burrito/internal/annotations"
+ "github.com/padok-team/burrito/internal/controllers/terraformpullrequest/comment"
+ log "github.com/sirupsen/logrus"
+ "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ ctrl "sigs.k8s.io/controller-runtime"
+)
+
+type State interface {
+ getHandler() func(ctx context.Context, r *Reconciler, repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest) ctrl.Result
+}
+
+func (r *Reconciler) GetState(ctx context.Context, pr *configv1alpha1.TerraformPullRequest) (State, []metav1.Condition) {
+ log := log.WithContext(ctx)
+ c1, isLastCommitDiscovered := r.IsLastCommitDiscovered(pr)
+ c2, areLayersStillPlanning := r.AreLayersStillPlanning(pr)
+ c3, isCommentUpToDate := r.IsCommentUpToDate(pr)
+ conditions := []metav1.Condition{c1, c2, c3}
+ switch {
+ case !isLastCommitDiscovered:
+ log.Infof("pull request %s needs to be discovered", pr.Name)
+ return &DiscoveryNeeded{}, conditions
+ case isLastCommitDiscovered && isCommentUpToDate:
+ log.Infof("pull request %s comment is up to date", pr.Name)
+ return &Idle{}, conditions
+ case isLastCommitDiscovered && areLayersStillPlanning:
+ log.Infof("pull request %s layers are still planning, waiting", pr.Name)
+ return &Idle{}, conditions
+ case isLastCommitDiscovered && !areLayersStillPlanning && !isCommentUpToDate:
+ log.Infof("pull request %s layers have finished, posting comment", pr.Name)
+ return &CommentNeeded{}, conditions
+ default:
+ log.Infof("pull request %s is in an unknown state, defaulting to idle. If this happens please file an issue, this is not an intended behavior.", pr.Name)
+ return &Idle{}, conditions
+ }
+}
+
+type Idle struct{}
+
+func (s *Idle) getHandler() func(ctx context.Context, r *Reconciler, repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest) ctrl.Result {
+ return func(ctx context.Context, r *Reconciler, repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest) ctrl.Result {
+ return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.WaitAction}
+ }
+}
+
+type DiscoveryNeeded struct{}
+
+func (s *DiscoveryNeeded) getHandler() func(ctx context.Context, r *Reconciler, repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest) ctrl.Result {
+ return func(ctx context.Context, r *Reconciler, repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest) ctrl.Result {
+ layers, err := r.getAffectedLayers(repository, pr)
+ if err != nil {
+ log.Errorf("failed to get affected layers for pull request %s: %s", pr.Name, err)
+ return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.OnError}
+ }
+ newLayers := generateTempLayers(pr, layers)
+ for _, layer := range newLayers {
+ err := r.Client.Create(ctx, &layer)
+ if errors.IsAlreadyExists(err) {
+ log.Infof("layer %s already exists, updating it", layer.Name)
+ err = r.Client.Update(ctx, &layer)
+ if err != nil {
+ log.Errorf("failed to update layer %s: %s", layer.Name, err)
+ return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.OnError}
+ }
+ }
+ if err != nil {
+ log.Errorf("failed to create layer %s: %s", layer.Name, err)
+ return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.OnError}
+ }
+ }
+ err = annotations.Add(ctx, r.Client, pr, map[string]string{annotations.LastDiscoveredCommit: pr.Annotations[annotations.LastBranchCommit]})
+ if err != nil {
+ log.Errorf("failed to add annotation %s to pull request %s: %s", annotations.LastDiscoveredCommit, pr.Name, err)
+ return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.OnError}
+ }
+ return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.WaitAction}
+ }
+}
+
+type CommentNeeded struct{}
+
+func (s *CommentNeeded) getHandler() func(ctx context.Context, r *Reconciler, repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest) ctrl.Result {
+ return func(ctx context.Context, r *Reconciler, repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest) ctrl.Result {
+ layers, err := getLinkedLayers(r.Client, pr)
+ if err != nil {
+ log.Errorf("failed to get linked layers for pull request %s: %s", pr.Name, err)
+ return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.OnError}
+ }
+
+ var provider Provider
+ found := false
+ for _, p := range r.Providers {
+ if p.IsFromProvider(pr) {
+ provider = p
+ found = true
+ }
+ }
+ if !found {
+ log.Infof("failed to get pull request provider. Requeuing")
+ return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.WaitAction}
+ }
+
+ comment := comment.NewDefaultComment(layers, r.Storage)
+ err = provider.Comment(repository, pr, comment)
+ if err != nil {
+ log.Errorf("an error occured while commenting pull request: %s", err)
+ return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.OnError}
+ }
+ err = annotations.Add(ctx, r.Client, pr, map[string]string{annotations.LastCommentedCommit: pr.Annotations[annotations.LastDiscoveredCommit]})
+ if err != nil {
+ log.Errorf("failed to add annotation %s to pull request %s: %s", annotations.LastCommentedCommit, pr.Name, err)
+ return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.OnError}
+ }
+ return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.WaitAction}
+ }
+}
+
+func getStateString(state State) string {
+ t := strings.Split(fmt.Sprintf("%T", state), ".")
+ return t[len(t)-1]
+}
diff --git a/internal/e2e/testdata/terraform/random-pets/test-plan b/internal/e2e/testdata/terraform/random-pets/test-plan
new file mode 100644
index 000000000..998f1cd5f
Binary files /dev/null and b/internal/e2e/testdata/terraform/random-pets/test-plan differ
diff --git a/internal/runner/runner.go b/internal/runner/runner.go
index f9e4e0a9a..066b49217 100644
--- a/internal/runner/runner.go
+++ b/internal/runner/runner.go
@@ -52,7 +52,7 @@ type TerraformExec interface {
Init(string) error
Plan() error
Apply() error
- Show() ([]byte, error)
+ Show(string) ([]byte, error)
}
func New(c *config.Config) *Runner {
@@ -61,13 +61,15 @@ func New(c *config.Config) *Runner {
}
}
+func (r *Runner) unlock() {
+ err := lock.DeleteLock(context.TODO(), r.client, r.layer)
+ if err != nil {
+ log.Fatalf("could not remove lease lock for terraform layer %s: %s", r.layer.Name, err)
+ }
+}
+
func (r *Runner) Exec() {
- defer func() {
- err := lock.DeleteLock(context.TODO(), r.client, r.layer)
- if err != nil {
- log.Fatalf("could not remove lease lock for terraform layer %s: %s", r.layer.Name, err)
- }
- }()
+ defer r.unlock()
var sum string
err := r.init()
ann := map[string]string{}
@@ -106,7 +108,7 @@ func (r *Runner) Exec() {
number++
ann[annotations.Failure] = strconv.Itoa(number)
}
- err = annotations.Add(context.TODO(), r.client, *r.layer, ann)
+ err = annotations.Add(context.TODO(), r.client, r.layer, ann)
if err != nil {
log.Errorf("could not update terraform layer annotations: %s", err)
}
@@ -217,11 +219,22 @@ func (r *Runner) plan() (string, error) {
log.Errorf("error executing terraform plan: %s", err)
return "", err
}
- planJsonBytes, err := r.exec.Show()
+ planJsonBytes, err := r.exec.Show("json")
if err != nil {
log.Errorf("error getting terraform plan json: %s", err)
return "", err
}
+ prettyPlan, err := r.exec.Show("pretty")
+ if err != nil {
+ log.Errorf("error getting terraform pretty plan: %s", err)
+ return "", err
+ }
+ prettyPlanKey := storage.GenerateKey(storage.LastPrettyPlan, r.layer)
+ log.Infof("setting pretty plan into storage at key %s", prettyPlanKey)
+ err = r.storage.Set(prettyPlanKey, prettyPlan, 3600)
+ if err != nil {
+ log.Errorf("could not put pretty plan in cache: %s", err)
+ }
plan := &tfjson.Plan{}
err = json.Unmarshal(planJsonBytes, plan)
if err != nil {
diff --git a/internal/runner/terraform/terraform.go b/internal/runner/terraform/terraform.go
index 736aac61a..9b7531d3e 100644
--- a/internal/runner/terraform/terraform.go
+++ b/internal/runner/terraform/terraform.go
@@ -3,6 +3,7 @@ package terraform
import (
"context"
"encoding/json"
+ "errors"
"io"
"os"
@@ -74,17 +75,28 @@ func (t *Terraform) Apply() error {
return nil
}
-func (t *Terraform) Show() ([]byte, error) {
+func (t *Terraform) Show(mode string) ([]byte, error) {
t.silent()
- planJson, err := t.exec.ShowPlanFile(context.TODO(), t.planArtifactPath)
- if err != nil {
- return nil, err
- }
- planJsonBytes, err := json.Marshal(planJson)
- if err != nil {
- return nil, err
+ switch mode {
+ case "json":
+ planJson, err := t.exec.ShowPlanFile(context.TODO(), t.planArtifactPath)
+ if err != nil {
+ return nil, err
+ }
+ planJsonBytes, err := json.Marshal(planJson)
+ if err != nil {
+ return nil, err
+ }
+ return planJsonBytes, nil
+ case "pretty":
+ plan, err := t.exec.ShowPlanFileRaw(context.TODO(), t.planArtifactPath)
+ if err != nil {
+ return nil, err
+ }
+ return []byte(plan), nil
+ default:
+ return nil, errors.New("invalid mode")
}
- return planJsonBytes, nil
}
func (t *Terraform) silent() {
diff --git a/internal/runner/terragrunt/terragrunt.go b/internal/runner/terragrunt/terragrunt.go
index a745bc492..1aa70703f 100644
--- a/internal/runner/terragrunt/terragrunt.go
+++ b/internal/runner/terragrunt/terragrunt.go
@@ -1,6 +1,7 @@
package terragrunt
import (
+ "errors"
"fmt"
"io"
"net/http"
@@ -53,6 +54,7 @@ func (t *Terragrunt) getDefaultOptions(command string) []string {
t.terraform.ExecPath,
"--terragrunt-working-dir",
t.workingDir,
+ "-no-color",
}
}
@@ -89,15 +91,24 @@ func (t *Terragrunt) Apply() error {
return nil
}
-func (t *Terragrunt) Show() ([]byte, error) {
- options := append(t.getDefaultOptions("show"), "-json", t.planArtifactPath)
+func (t *Terragrunt) Show(mode string) ([]byte, error) {
+ options := t.getDefaultOptions("show")
+ switch mode {
+ case "json":
+ options = append(options, "-json", t.planArtifactPath)
+ case "pretty":
+ options = append(options, t.planArtifactPath)
+ default:
+ return nil, errors.New("invalid mode")
+ }
cmd := exec.Command(t.execPath, options...)
cmd.Dir = t.workingDir
- jsonBytes, err := cmd.Output()
+ output, err := cmd.Output()
+
if err != nil {
return nil, err
}
- return jsonBytes, nil
+ return output, nil
}
func downloadTerragrunt(version string) (string, error) {
diff --git a/internal/storage/common.go b/internal/storage/common.go
index b858ef206..653e58272 100644
--- a/internal/storage/common.go
+++ b/internal/storage/common.go
@@ -35,6 +35,7 @@ const (
RunMessage Prefix = "runMessage"
LastPlannedArtifactJson Prefix = "plannedArtifactJson"
LastPlanResult Prefix = "planResult"
+ LastPrettyPlan Prefix = "prettyPlan"
)
type Storage interface {
@@ -44,23 +45,8 @@ type Storage interface {
}
func GenerateKey(prefix Prefix, layer *configv1alpha1.TerraformLayer) string {
- var toHash string
- switch prefix {
- case LastPlannedArtifactBin:
- toHash = layer.Spec.Repository.Name + layer.Spec.Repository.Namespace + layer.Spec.Path + layer.Spec.Branch
- return fmt.Sprintf("%s-%d", prefix, hash(toHash))
- case LastPlannedArtifactJson:
- toHash = layer.Spec.Repository.Name + layer.Spec.Repository.Namespace + layer.Spec.Path + layer.Spec.Branch
- return fmt.Sprintf("%s-%d", prefix, hash(toHash))
- case LastPlanResult:
- toHash = layer.Spec.Repository.Name + layer.Spec.Repository.Namespace + layer.Spec.Path + layer.Spec.Branch
- return fmt.Sprintf("%s-%d", prefix, hash(toHash))
- case RunMessage:
- toHash = layer.Spec.Repository.Name + layer.Spec.Repository.Namespace + layer.Spec.Path + layer.Spec.Branch
- return fmt.Sprintf("%s-%d", prefix, hash(toHash))
- default:
- return ""
- }
+ toHash := layer.Spec.Repository.Name + layer.Spec.Repository.Namespace + layer.Spec.Path + layer.Spec.Branch
+ return fmt.Sprintf("%s-%d", prefix, hash(toHash))
}
func hash(s string) uint32 {
diff --git a/internal/webhook/event/common.go b/internal/webhook/event/common.go
new file mode 100644
index 000000000..2a522a1d9
--- /dev/null
+++ b/internal/webhook/event/common.go
@@ -0,0 +1,81 @@
+package event
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+const PullRequestOpened = "opened"
+const PullRequestClosed = "closed"
+
+type ChangeInfo struct {
+ ShaBefore string
+ ShaAfter string
+}
+
+type Event interface {
+ Handle(client.Client) error
+}
+
+// Normalize a Github/Gitlab URL (SSH or HTTPS) to a HTTPS URL
+func NormalizeUrl(url string) string {
+ if strings.Contains(url, "https://") {
+ return url
+ }
+ // All SSH URL from GitHub are like "git@padok.github.com:/.git"
+ // We split on ":" then remove ".git" by removing the last characters
+ // To handle enterprise GitHub, we dynamically get "padok.github.com"
+ // By removing "git@" at the beginning of the string
+ split := strings.Split(url, ":")
+ return fmt.Sprintf("https://%s/%s", split[0][4:], split[1][:len(split[1])-4])
+}
+
+func ParseRevision(ref string) string {
+ refParts := strings.SplitN(ref, "/", 3)
+ return refParts[len(refParts)-1]
+}
+
+func isLayerLinkedToAnyRepositories(repositories []configv1alpha1.TerraformRepository, layer configv1alpha1.TerraformLayer) bool {
+ for _, r := range repositories {
+ if r.Name == layer.Spec.Repository.Name && r.Namespace == layer.Spec.Repository.Namespace {
+ return true
+ }
+ }
+ return false
+}
+
+func layerFilesHaveChanged(layer configv1alpha1.TerraformLayer, changedFiles []string) bool {
+ if len(changedFiles) == 0 {
+ return true
+ }
+
+ // At last one changed file must be under refresh path
+ for _, f := range changedFiles {
+ f = ensureAbsPath(f)
+ if strings.Contains(f, layer.Spec.Path) {
+ return true
+ }
+ }
+
+ return false
+}
+
+func isPRLinkedToAnyRepositories(pr configv1alpha1.TerraformPullRequest, repos []configv1alpha1.TerraformRepository) bool {
+ for _, r := range repos {
+ if r.Name == pr.Spec.Repository.Name && r.Namespace == pr.Spec.Repository.Namespace {
+ return true
+ }
+ }
+ return false
+}
+
+func ensureAbsPath(input string) string {
+ if !filepath.IsAbs(input) {
+ return string(filepath.Separator) + input
+ }
+ return input
+}
diff --git a/internal/webhook/event/event_test.go b/internal/webhook/event/event_test.go
new file mode 100644
index 000000000..5dfbc1a62
--- /dev/null
+++ b/internal/webhook/event/event_test.go
@@ -0,0 +1 @@
+package event_test
diff --git a/internal/webhook/event/pullrequest.go b/internal/webhook/event/pullrequest.go
new file mode 100644
index 000000000..80a4c9240
--- /dev/null
+++ b/internal/webhook/event/pullrequest.go
@@ -0,0 +1,111 @@
+package event
+
+import (
+ "context"
+ "fmt"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1"
+ "github.com/padok-team/burrito/internal/annotations"
+ log "github.com/sirupsen/logrus"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "github.com/hashicorp/go-multierror"
+)
+
+type PullRequestEvent struct {
+ Provider string
+ URL string
+ Revision string
+ Base string
+ Action string
+ ID string
+ Commit string
+}
+
+func (e *PullRequestEvent) Handle(c client.Client) error {
+ repositories := &configv1alpha1.TerraformRepositoryList{}
+ err := c.List(context.Background(), repositories)
+ if err != nil {
+ log.Errorf("could not list terraform repositories: %s", err)
+ return err
+ }
+ affectedRepositories := e.getAffectedRepositories(repositories.Items)
+ if len(affectedRepositories) == 0 {
+ log.Infof("no affected repositories found for pull request event")
+ return nil
+ }
+ prs := e.generateTerraformPullRequests(affectedRepositories)
+ switch e.Action {
+ case PullRequestOpened:
+ return batchCreatePullRequests(context.TODO(), c, prs)
+ case PullRequestClosed:
+ return batchDeletePullRequests(context.TODO(), c, prs)
+ default:
+ log.Infof("action %s not supported", e.Action)
+ }
+ return nil
+}
+
+func batchCreatePullRequests(ctx context.Context, c client.Client, prs []configv1alpha1.TerraformPullRequest) error {
+ var errResult error
+ for _, pr := range prs {
+ err := c.Create(ctx, &pr)
+ if err != nil {
+ errResult = multierror.Append(errResult, err)
+ }
+ }
+ return errResult
+}
+
+func batchDeletePullRequests(ctx context.Context, c client.Client, prs []configv1alpha1.TerraformPullRequest) error {
+ var errResult error
+ for _, pr := range prs {
+ err := c.Delete(ctx, &pr)
+ if err != nil {
+ errResult = multierror.Append(errResult, err)
+ }
+ }
+ return errResult
+}
+
+func (e *PullRequestEvent) generateTerraformPullRequests(repositories []configv1alpha1.TerraformRepository) []configv1alpha1.TerraformPullRequest {
+ prs := []configv1alpha1.TerraformPullRequest{}
+ for _, repository := range repositories {
+ pr := configv1alpha1.TerraformPullRequest{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("%s-%s", repository.Name, e.ID),
+ Namespace: repository.Namespace,
+ Annotations: map[string]string{
+ annotations.LastBranchCommit: e.Commit,
+ },
+ },
+ Spec: configv1alpha1.TerraformPullRequestSpec{
+ Provider: e.Provider,
+ Branch: e.Revision,
+ ID: e.ID,
+ Base: e.Base,
+ Repository: configv1alpha1.TerraformLayerRepository{
+ Name: repository.Name,
+ Namespace: repository.Namespace,
+ },
+ },
+ }
+ prs = append(prs, pr)
+ }
+ return prs
+}
+
+// Function that checks if any TerraformRepository is linked to a PullRequestEvent
+func (e *PullRequestEvent) getAffectedRepositories(repositories []configv1alpha1.TerraformRepository) []configv1alpha1.TerraformRepository {
+ affectedRepositories := []configv1alpha1.TerraformRepository{}
+ for _, repo := range repositories {
+ log.Infof("evaluating terraform repository %s for url %s", repo.Name, repo.Spec.Repository.Url)
+ log.Infof("comparing noramlized url %s with received URL from paylaod %s", NormalizeUrl(repo.Spec.Repository.Url), e.URL)
+ if e.URL == NormalizeUrl(repo.Spec.Repository.Url) {
+ affectedRepositories = append(affectedRepositories, repo)
+ }
+ }
+ return affectedRepositories
+}
diff --git a/internal/webhook/event/push.go b/internal/webhook/event/push.go
new file mode 100644
index 000000000..265b09ab6
--- /dev/null
+++ b/internal/webhook/event/push.go
@@ -0,0 +1,109 @@
+package event
+
+import (
+ "context"
+
+ configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1"
+ "github.com/padok-team/burrito/internal/annotations"
+ log "github.com/sirupsen/logrus"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+type PushEvent struct {
+ URL string
+ Revision string
+ ChangeInfo
+ Changes []string
+}
+
+func (e *PushEvent) Handle(c client.Client) error {
+ repositories := &configv1alpha1.TerraformRepositoryList{}
+ err := c.List(context.Background(), repositories)
+ if err != nil {
+ log.Errorf("could not list terraform repositories: %s", err)
+ return err
+ }
+ layers := &configv1alpha1.TerraformLayerList{}
+ err = c.List(context.Background(), layers)
+ if err != nil {
+ log.Errorf("could not list terraform layers: %s", err)
+ return err
+ }
+ prs := &configv1alpha1.TerraformPullRequestList{}
+ err = c.List(context.Background(), prs)
+ if err != nil {
+ log.Errorf("could not list terraform prs: %s", err)
+ return err
+ }
+ affectedRepositories := e.getAffectedRepositories(repositories.Items)
+ for _, repo := range affectedRepositories {
+ ann := map[string]string{}
+ ann[annotations.LastBranchCommit] = e.ChangeInfo.ShaAfter
+ err := annotations.Add(context.TODO(), c, &repo, ann)
+ if err != nil {
+ log.Errorf("could not add annotation to terraform repository %s", err)
+ return err
+ }
+ }
+
+ for _, layer := range e.getAffectedLayers(layers.Items, repositories.Items) {
+ ann := map[string]string{}
+ log.Printf("evaluating terraform layer %s for revision %s", layer.Name, e.Revision)
+ if layer.Spec.Branch != e.Revision {
+ log.Infof("branch %s for terraform layer %s not matching revision %s", layer.Spec.Branch, layer.Name, e.Revision)
+ continue
+ }
+ ann[annotations.LastBranchCommit] = e.ChangeInfo.ShaAfter
+ if layerFilesHaveChanged(layer, e.Changes) {
+ ann[annotations.LastRelevantCommit] = e.ChangeInfo.ShaAfter
+ }
+
+ err := annotations.Add(context.TODO(), c, &layer, ann)
+ if err != nil {
+ log.Errorf("could not add annotation to terraform layer %s", err)
+ return err
+ }
+ }
+
+ for _, pr := range e.getAffectedPullRequests(prs.Items, affectedRepositories) {
+ ann := map[string]string{}
+ ann[annotations.LastBranchCommit] = e.ChangeInfo.ShaAfter
+ err := annotations.Add(context.TODO(), c, &pr, ann)
+ if err != nil {
+ log.Errorf("could not add annotation to terraform pr %s", err)
+ return err
+ }
+ }
+ return nil
+}
+
+func (e *PushEvent) getAffectedRepositories(repositories []configv1alpha1.TerraformRepository) []configv1alpha1.TerraformRepository {
+ affectedRepositories := []configv1alpha1.TerraformRepository{}
+ for _, repo := range repositories {
+ if e.URL == NormalizeUrl(repo.Spec.Repository.Url) {
+ affectedRepositories = append(affectedRepositories, repo)
+ continue
+ }
+ }
+ return affectedRepositories
+}
+
+func (e *PushEvent) getAffectedLayers(allLayers []configv1alpha1.TerraformLayer, affectedRepositories []configv1alpha1.TerraformRepository) []configv1alpha1.TerraformLayer {
+ layers := []configv1alpha1.TerraformLayer{}
+ for _, layer := range allLayers {
+ if isLayerLinkedToAnyRepositories(affectedRepositories, layer) {
+ layers = append(layers, layer)
+ }
+ }
+ return layers
+}
+
+func (e *PushEvent) getAffectedPullRequests(prs []configv1alpha1.TerraformPullRequest, affectedRepositories []configv1alpha1.TerraformRepository) []configv1alpha1.TerraformPullRequest {
+ affectedPRs := []configv1alpha1.TerraformPullRequest{}
+ for _, pr := range prs {
+ if isPRLinkedToAnyRepositories(pr, affectedRepositories) && pr.Spec.Branch == e.Revision {
+ affectedPRs = append(affectedPRs, pr)
+ }
+ }
+ return affectedPRs
+}
diff --git a/internal/webhook/github/provider.go b/internal/webhook/github/provider.go
new file mode 100644
index 000000000..cc732ddf4
--- /dev/null
+++ b/internal/webhook/github/provider.go
@@ -0,0 +1,76 @@
+package github
+
+import (
+ "errors"
+ "net/http"
+ "strconv"
+
+ "github.com/go-playground/webhooks/github"
+ "github.com/padok-team/burrito/internal/burrito/config"
+ "github.com/padok-team/burrito/internal/webhook/event"
+
+ log "github.com/sirupsen/logrus"
+)
+
+type Github struct {
+ github *github.Webhook
+}
+
+func (g *Github) Init(c *config.Config) error {
+ githubWebhook, err := github.New(github.Options.Secret(c.Server.Webhook.Github.Secret))
+ if err != nil {
+ return err
+ }
+ g.github = githubWebhook
+ return nil
+}
+
+func (g *Github) IsFromProvider(r *http.Request) bool {
+ return r.Header.Get("X-GitHub-Event") != ""
+}
+
+func (g *Github) GetEvent(r *http.Request) (event.Event, error) {
+ p, err := g.github.Parse(r, github.PushEvent, github.PingEvent, github.PullRequestEvent)
+ if errors.Is(err, github.ErrHMACVerificationFailed) {
+ log.Errorf("GitHub webhook HMAC verification failed: %s", err)
+ return nil, err
+ }
+ var e event.Event
+ switch payload := p.(type) {
+ case github.PushPayload:
+ log.Infof("parsing Github push event payload")
+ changedFiles := []string{}
+ for _, commit := range payload.Commits {
+ changedFiles = append(changedFiles, commit.Added...)
+ changedFiles = append(changedFiles, commit.Modified...)
+ changedFiles = append(changedFiles, commit.Removed...)
+ }
+ e = &event.PushEvent{
+ URL: event.NormalizeUrl(payload.Repository.HTMLURL),
+ Revision: event.ParseRevision(payload.Ref),
+ ChangeInfo: event.ChangeInfo{
+ ShaBefore: payload.Before,
+ ShaAfter: payload.After,
+ },
+ Changes: changedFiles,
+ }
+ case github.PullRequestPayload:
+ log.Infof("parsing Github pull request event payload")
+ if err != nil {
+ log.Warnf("could not retrieve pull request from Github API: %s", err)
+ return nil, err
+ }
+ e = &event.PullRequestEvent{
+ Provider: "github",
+ ID: strconv.FormatInt(payload.PullRequest.Number, 10),
+ URL: event.NormalizeUrl(payload.Repository.HTMLURL),
+ Revision: event.ParseRevision(payload.PullRequest.Head.Ref),
+ Action: payload.Action,
+ Base: payload.PullRequest.Base.Ref,
+ Commit: payload.PullRequest.Head.Sha,
+ }
+ default:
+ return nil, errors.New("unsupported Event")
+ }
+ return e, nil
+}
diff --git a/internal/webhook/gitlab/provider.go b/internal/webhook/gitlab/provider.go
new file mode 100644
index 000000000..4d3398352
--- /dev/null
+++ b/internal/webhook/gitlab/provider.go
@@ -0,0 +1,81 @@
+package gitlab
+
+import (
+ "errors"
+ "net/http"
+ "strconv"
+
+ "github.com/go-playground/webhooks/gitlab"
+ "github.com/padok-team/burrito/internal/burrito/config"
+ "github.com/padok-team/burrito/internal/webhook/event"
+ log "github.com/sirupsen/logrus"
+)
+
+type Gitlab struct {
+ gitlab *gitlab.Webhook
+}
+
+func (g *Gitlab) Init(c *config.Config) error {
+ gitlabWebhook, err := gitlab.New(gitlab.Options.Secret(c.Server.Webhook.Gitlab.Secret))
+ if err != nil {
+ return err
+ }
+ g.gitlab = gitlabWebhook
+ return nil
+}
+
+func (g *Gitlab) IsFromProvider(r *http.Request) bool {
+ return r.Header.Get("X-Gitlab-Event") != ""
+}
+
+func (g *Gitlab) GetEvent(r *http.Request) (event.Event, error) {
+ var e event.Event
+ p, err := g.gitlab.Parse(r, gitlab.PushEvents, gitlab.TagEvents, gitlab.MergeRequestEvents)
+ if errors.Is(err, gitlab.ErrGitLabTokenVerificationFailed) {
+ log.Errorf("GitLab webhook token verification failed: %s", err)
+ }
+ switch payload := p.(type) {
+ case gitlab.PushEventPayload:
+ log.Infof("parsing Gitlab push event payload")
+ changedFiles := []string{}
+ for _, commit := range payload.Commits {
+ changedFiles = append(changedFiles, commit.Added...)
+ changedFiles = append(changedFiles, commit.Modified...)
+ changedFiles = append(changedFiles, commit.Removed...)
+ }
+ e = &event.PushEvent{
+ URL: event.NormalizeUrl(payload.Project.WebURL),
+ Revision: event.ParseRevision(payload.Ref),
+ ChangeInfo: event.ChangeInfo{
+ ShaBefore: payload.Before,
+ ShaAfter: payload.After,
+ },
+ Changes: changedFiles,
+ }
+ case gitlab.MergeRequestEventPayload:
+ log.Infof("parsing Gitlab merge request event payload")
+ e = &event.PullRequestEvent{
+ Provider: "gitlab",
+ ID: strconv.Itoa(int(payload.ObjectAttributes.ID)),
+ URL: event.NormalizeUrl(payload.Project.WebURL),
+ Revision: event.ParseRevision(payload.ObjectAttributes.Ref),
+ Action: getNormalizedAction(payload.ObjectAttributes.Action),
+ Base: payload.ObjectAttributes.TargetBranch,
+ Commit: payload.ObjectAttributes.LastCommit.ID,
+ }
+ default:
+ return nil, errors.New("unsupported event")
+ }
+ return e, nil
+}
+
+func getNormalizedAction(action string) string {
+ switch action {
+ case "open":
+ return event.PullRequestOpened
+ case "close":
+ return event.PullRequestClosed
+ default:
+ return action
+ }
+}
diff --git a/internal/webhook/webhook.go b/internal/webhook/webhook.go
index 22d5d25b6..5746e4dbb 100644
--- a/internal/webhook/webhook.go
+++ b/internal/webhook/webhook.go
@@ -1,28 +1,24 @@
package webhook
import (
- "context"
- "errors"
"fmt"
"html"
"net/http"
- "path/filepath"
- "strings"
log "github.com/sirupsen/logrus"
- "github.com/go-playground/webhooks/v6/github"
- "github.com/go-playground/webhooks/v6/gitlab"
- "github.com/padok-team/burrito/internal/annotations"
"github.com/padok-team/burrito/internal/burrito/config"
- "k8s.io/apimachinery/pkg/runtime"
+ "github.com/padok-team/burrito/internal/webhook/event"
+ "github.com/padok-team/burrito/internal/webhook/github"
+ "github.com/padok-team/burrito/internal/webhook/gitlab"
- ctrl "sigs.k8s.io/controller-runtime"
- "sigs.k8s.io/controller-runtime/pkg/client"
+ "k8s.io/apimachinery/pkg/runtime"
configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
)
type Handler interface {
@@ -31,9 +27,8 @@ type Handler interface {
type Webhook struct {
client.Client
- config *config.Config
- github *github.Webhook
- gitlab *gitlab.Webhook
+ config *config.Config
+ providers []Provider
}
func New(c *config.Config) *Webhook {
@@ -42,6 +37,12 @@ func New(c *config.Config) *Webhook {
}
}
+type Provider interface {
+ Init(*config.Config) error
+ IsFromProvider(*http.Request) bool
+ GetEvent(*http.Request) (event.Event, error)
+}
+
func (w *Webhook) Init() error {
scheme := runtime.NewScheme()
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
@@ -53,171 +54,58 @@ func (w *Webhook) Init() error {
return err
}
w.Client = cl
- githubWebhook, err := github.New(github.Options.Secret(w.config.Server.Webhook.Github.Secret))
- if err != nil {
- return err
+ providers := []Provider{}
+ for _, p := range []Provider{&github.Github{}, &gitlab.Gitlab{}} {
+ err = p.Init(w.config)
+ if err != nil {
+ log.Warnf("failed to initialize webhook provider: %s", err)
+ continue
+ }
+ providers = append(providers, p)
}
- w.github = githubWebhook
- gitlabWebhook, err := gitlab.New(gitlab.Options.Secret(w.config.Server.Webhook.Gitlab.Secret))
- if err != nil {
- return err
+ if len(providers) == 0 {
+ log.Warnf("no webhook provider initialized, every event will be considered as unknown")
}
- w.gitlab = gitlabWebhook
+ w.providers = providers
return nil
}
func (w *Webhook) GetHttpHandler() func(http.ResponseWriter, *http.Request) {
log.Infof("webhook event received...")
return func(writer http.ResponseWriter, r *http.Request) {
- var payload interface{}
var err error
-
- switch {
- case r.Header.Get("X-GitHub-Event") != "":
- log.Infof("webhook has detected a GitHub event")
- payload, err = w.github.Parse(r, github.PushEvent, github.PingEvent)
- if errors.Is(err, github.ErrHMACVerificationFailed) {
- log.Errorf("GitHub webhook HMAC verification failed: %s", err)
- }
- case r.Header.Get("X-Gitlab-Event") != "":
- log.Infof("webhook has detected a GitLab event")
- payload, err = w.gitlab.Parse(r, gitlab.PushEvents, gitlab.TagEvents)
- if errors.Is(err, gitlab.ErrGitLabTokenVerificationFailed) {
- log.Errorf("GitLab webhook token verification failed: %s", err)
+ var event event.Event
+ for _, p := range w.providers {
+ if p.IsFromProvider(r) {
+ event, err = p.GetEvent(r)
+ break
}
- default:
- log.Infof("ignoring unknown webhook event")
- http.Error(writer, "Unknown webhook event", http.StatusBadRequest)
- return
}
-
if err != nil {
log.Errorf("webhook processing failed: %s", err)
status := http.StatusBadRequest
if r.Method != "POST" {
status = http.StatusMethodNotAllowed
}
- http.Error(writer, fmt.Sprintf("Webhook processing failed: %s", html.EscapeString(err.Error())), status)
+ http.Error(writer, fmt.Sprintf("webhook processing failed: %s", html.EscapeString(err.Error())), status)
return
}
-
- w.Handle(payload)
- }
-}
-
-func (w *Webhook) Handle(payload interface{}) {
- webUrls, sshUrls, revision, change, touchedHead, changedFiles := affectedRevisionInfo(payload)
- allUrls := append(webUrls, sshUrls...)
-
- if len(allUrls) == 0 {
- log.Infof("ignoring webhook event")
- return
- }
- for _, url := range allUrls {
- log.Infof("received event repo: %s, revision: %s, touchedHead: %v", url, revision, touchedHead)
- }
-
- repositories := &configv1alpha1.TerraformRepositoryList{}
- err := w.Client.List(context.TODO(), repositories)
- if err != nil {
- log.Errorf("could not get terraform repositories: %s", err)
- }
-
- for _, url := range allUrls {
- for _, repo := range repositories.Items {
- log.Infof("evaluating terraform repository %s for url %s", repo.Name, url)
- if repo.Spec.Repository.Url != url {
- log.Infof("evaluating terraform repository %s url %s not matching %s", repo.Name, repo.Spec.Repository.Url, url)
- continue
- }
- layers := &configv1alpha1.TerraformLayerList{}
- err := w.Client.List(context.TODO(), layers, &client.ListOptions{})
- if err != nil {
- log.Errorf("could not get terraform layers: %s", err)
- }
- for _, layer := range layers.Items {
- ann := map[string]string{}
- log.Printf("evaluating terraform layer %s for revision %s", layer.Name, revision)
- if layer.Spec.Branch != revision {
- log.Infof("branch %s for terraform layer %s not matching revision %s", layer.Spec.Branch, layer.Name, revision)
- continue
- }
- ann[annotations.LastBranchCommit] = change.shaAfter
- if layerFilesHaveChanged(&layer, changedFiles) {
- ann[annotations.LastRelevantCommit] = change.shaAfter
- }
- err = annotations.Add(context.TODO(), w.Client, layer, ann)
- if err != nil {
- log.Errorf("could not add annotation to terraform layer %s", err)
- }
- }
- }
- }
- return
-}
-
-type changeInfo struct {
- shaBefore string
- shaAfter string
-}
-
-func parseRevision(ref string) string {
- refParts := strings.SplitN(ref, "/", 3)
- return refParts[len(refParts)-1]
-}
-
-func affectedRevisionInfo(payloadIf interface{}) (webUrls []string, sshUrls []string, revision string, change changeInfo, touchedHead bool, changedFiles []string) {
- switch payload := payloadIf.(type) {
- case github.PushPayload:
- log.Infof("parsing GitHub push event payload")
- webUrls = append(webUrls, payload.Repository.HTMLURL)
- sshUrls = append(sshUrls, payload.Repository.SSHURL)
- revision = parseRevision(payload.Ref)
- change.shaAfter = parseRevision(payload.After)
- change.shaBefore = parseRevision(payload.Before)
- touchedHead = bool(payload.Repository.DefaultBranch == revision)
- for _, commit := range payload.Commits {
- changedFiles = append(changedFiles, commit.Added...)
- changedFiles = append(changedFiles, commit.Modified...)
- changedFiles = append(changedFiles, commit.Removed...)
- }
- case gitlab.PushEventPayload:
- log.Infof("parsing GitLab push event payload")
- webUrls = append(webUrls, payload.Project.WebURL)
- revision = parseRevision(payload.Ref)
- change.shaAfter = parseRevision(payload.After)
- change.shaBefore = parseRevision(payload.Before)
- touchedHead = bool(payload.Project.DefaultBranch == revision)
- for _, commit := range payload.Commits {
- changedFiles = append(changedFiles, commit.Added...)
- changedFiles = append(changedFiles, commit.Modified...)
- changedFiles = append(changedFiles, commit.Removed...)
+ if event == nil {
+ log.Infof("ignoring unknown webhook event")
+ http.Error(writer, "Unknown webhook event", http.StatusBadRequest)
}
- default:
- log.Infof("event not handled")
- }
- return webUrls, sshUrls, revision, change, touchedHead, changedFiles
-}
-func layerFilesHaveChanged(layer *configv1alpha1.TerraformLayer, changedFiles []string) bool {
- if len(changedFiles) == 0 {
- return true
- }
-
- // At last one changed file must be under refresh path
- for _, f := range changedFiles {
- f = ensureAbsPath(f)
- if strings.Contains(f, layer.Spec.Path) {
- return true
+ err = w.Handle(event)
+ if err != nil {
+ log.Errorf("webhook processing worked but errored during event handling: %s", err)
}
}
-
- return false
}
-func ensureAbsPath(input string) string {
- if !filepath.IsAbs(input) {
- return string(filepath.Separator) + input
+func (w *Webhook) Handle(e event.Event) error {
+ err := e.Handle(w.Client)
+ if err != nil {
+ return err
}
- return input
+ return nil
}
diff --git a/manifests/install.yaml b/manifests/install.yaml
index a6e451b7b..9955eeb3e 100644
--- a/manifests/install.yaml
+++ b/manifests/install.yaml
@@ -1,6 +1,161 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.10.0
+ creationTimestamp: null
+ name: terraformpullrequests.config.terraform.padok.cloud
+spec:
+ group: config.terraform.padok.cloud
+ names:
+ kind: TerraformPullRequest
+ listKind: TerraformPullRequestList
+ plural: terraformpullrequests
+ shortNames:
+ - pr
+ - prs
+ - pullrequest
+ - pullrequests
+ singular: terraformpullrequest
+ scope: Namespaced
+ versions:
+ - additionalPrinterColumns:
+ - jsonPath: .spec.id
+ name: ID
+ type: string
+ - jsonPath: .status.state
+ name: State
+ type: string
+ - jsonPath: .spec.provider
+ name: Provider
+ type: string
+ - jsonPath: .spec.base
+ name: Base
+ type: string
+ - jsonPath: .spec.branch
+ name: Branch
+ type: string
+ name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: TerraformPullRequest is the Schema for the TerraformPullRequests
+ API
+ properties:
+ apiVersion:
+ description: 'APIVersion defines the versioned schema of this representation
+ of an object. Servers should convert recognized schemas to the latest
+ internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+ type: string
+ kind:
+ description: 'Kind is a string value representing the REST resource this
+ object represents. Servers may infer this from the endpoint the client
+ submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: TerraformPullRequestSpec defines the desired state of TerraformPullRequest
+ properties:
+ base:
+ type: string
+ branch:
+ type: string
+ id:
+ type: string
+ provider:
+ type: string
+ repository:
+ properties:
+ name:
+ type: string
+ namespace:
+ type: string
+ type: object
+ type: object
+ status:
+ description: TerraformPullRequestStatus defines the observed state of
+ TerraformPullRequest
+ properties:
+ conditions:
+ items:
+ description: "Condition contains details for one aspect of the current
+ state of this API Resource. --- This struct is intended for direct
+ use as an array at the field path .status.conditions. For example,
+ \n type FooStatus struct{ // Represents the observations of a
+ foo's current state. // Known .status.conditions.type are: \"Available\",
+ \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
+ // +listType=map // +listMapKey=type Conditions []metav1.Condition
+ `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
+ protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
+ properties:
+ lastTransitionTime:
+ description: lastTransitionTime is the last time the condition
+ transitioned from one status to another. This should be when
+ the underlying condition changed. If that is not known, then
+ using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: message is a human readable message indicating
+ details about the transition. This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: observedGeneration represents the .metadata.generation
+ that the condition was set based upon. For instance, if .metadata.generation
+ is currently 12, but the .status.conditions[x].observedGeneration
+ is 9, the condition is out of date with respect to the current
+ state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: reason contains a programmatic identifier indicating
+ the reason for the condition's last transition. Producers
+ of specific condition types may define expected values and
+ meanings for this field, and whether the values are considered
+ a guaranteed API. The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ --- Many .condition.type values are consistent across resources
+ like Available, but because arbitrary conditions can be useful
+ (see .node.status.conditions), the ability to deconflict is
+ important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
+ state:
+ type: string
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
@@ -247,7 +402,9 @@ spec:
- name
type: object
type: array
- x-kubernetes-list-type: set
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
limits:
additionalProperties:
anyOf:
@@ -953,7 +1110,9 @@ spec:
- name
type: object
type: array
- x-kubernetes-list-type: set
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
limits:
additionalProperties:
anyOf:
@@ -2332,7 +2491,9 @@ spec:
- name
type: object
type: array
- x-kubernetes-list-type: set
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
limits:
additionalProperties:
anyOf:
@@ -3038,7 +3199,9 @@ spec:
- name
type: object
type: array
- x-kubernetes-list-type: set
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
limits:
additionalProperties:
anyOf:
@@ -4056,8 +4219,6 @@ spec:
type: object
path:
type: string
- planOnPullRequest:
- type: boolean
remediationStrategy:
enum:
- dry
@@ -4065,8 +4226,6 @@ spec:
type: string
repository:
properties:
- kind:
- type: string
name:
type: string
namespace:
@@ -4290,65 +4449,31 @@ rules:
resources:
- terraformlayers
verbs:
- - create
- - delete
- get
- list
- patch
- update
- watch
-- apiGroups:
- - config.terraform.padok.cloud
- resources:
- - terraformlayers/finalizers
- verbs:
- - update
-- apiGroups:
- - config.terraform.padok.cloud
- resources:
- - terraformlayers/status
- verbs:
- - get
- - patch
- - update
- apiGroups:
- config.terraform.padok.cloud
resources:
- terraformrepositories
verbs:
- - create
- - delete
- get
- list
- - patch
- - update
- watch
- apiGroups:
- config.terraform.padok.cloud
resources:
- - terraformrepositories/finalizers
- verbs:
- - update
-- apiGroups:
- - config.terraform.padok.cloud
- resources:
- - terraformrepositories/status
+ - terraformpullrequests
verbs:
+ - create
+ - delete
- get
+ - list
- patch
- update
-- apiGroups:
- - "coordination.k8s.io"
- resources:
- - leases
- verbs:
- - get
- - list
- watch
- - create
- - update
- - patch
- - delete
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
@@ -4419,6 +4544,32 @@ rules:
- terraformrepositories/finalizers
verbs:
- update
+- apiGroups:
+ - config.terraform.padok.cloud
+ resources:
+ - terraformpullrequests
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - config.terraform.padok.cloud
+ resources:
+ - terraformpullrequests/finalizers
+ verbs:
+ - update
+- apiGroups:
+ - config.terraform.padok.cloud
+ resources:
+ - terraformpullrequests/status
+ verbs:
+ - get
+ - patch
+ - update
- apiGroups:
- config.terraform.padok.cloud
resources:
@@ -4647,7 +4798,7 @@ metadata:
data:
known_hosts: |-
bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAubiN81eDcafrgMeLzaFPsw2kNvEcqTKl/VqLat/MaB33pZy0y3rJZtnqwR2qOOvbwKZYKiEO1O6VqNEBxKvJJelCq0dTXWT5pbO2gDXC6h6QDXCaHo6pOHGPUy+YBaGQRGuSusMEASYiWunYN0vCAI8QaXnWMXNMdFP3jHAJH0eDsoiGnLPBlBp4TNm6rYI74nMzgz3B9IikW4WVK+dc8KZJZWYjAuORU3jc1c/NPskD2ASinf8v3xnfXeukU0sJ5N6m5E8VLjObPEO+mN2t/FZTMZLiFqPWc/ALSqnMnnhwrNi2rbfg/rd/IpL8Le3pSBne8+seeFVBoGqzHM9yXw==
- github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+ github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=
gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf
gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9