diff --git a/.codecov.yml b/.codecov.yml index 7a807b0..df18f8e 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,6 +1,4 @@ coverage: status: project: false - patch: false -comment: - layout: false \ No newline at end of file + patch: false \ No newline at end of file diff --git a/Makefile b/Makefile index 21d49d1..daa3e4b 100644 --- a/Makefile +++ b/Makefile @@ -126,11 +126,11 @@ COVERPKG = $(shell go list ./... | grep -v 'test' | grep -v -E "$(DEPREACTED_API .PHONY: test-behavior test-behavior: cleanup-tests ## Install the test env (gitea). Run the behavior tests against a Kind k8s instance that is spun up. Cleanup when finished. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./test/e2e/syngit -v -ginkgo.v -cover -coverpkg=$(COVERPKG) -coverprofile=coverage.txt + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./test/e2e/syngit -timeout 20m -v -ginkgo.v -cover -coverpkg=$(COVERPKG) -coverprofile=coverage.txt .PHONY: fast-behavior fast-behavior: ## Install the test env if not already installed. Run the behavior tests against a Kind k8s instance that is spun up. Does not cleanup when finished (meant to be run often). - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./test/e2e/syngit -v -ginkgo.v -cover -coverpkg=$(COVERPKG) -setup fast + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./test/e2e/syngit -timeout 20m -v -ginkgo.v -cover -coverpkg=$(COVERPKG) -setup fast .PHONY: test-selected test-selected: ## Install the test env if not already installed. Run only one selected test against a Kind k8s instance that is spun up. Does not cleanup when finished (meant to be run often). @@ -323,6 +323,10 @@ kind-load-image: ## Load the image in KinD. setup-gitea: ## Setup the 2 gitea platforms in the cluster ./test/utils/gitea/launch-gitea-setup.sh +.PHONY: reset-gitea +reset-gitea: ## Setup the 2 gitea platforms in the cluster + ./test/utils/gitea/reset-gitea-repos.sh + .PHONY: cleanup-gitea cleanup-gitea: ## Cleanup the 2 gitea platforms from the cluster. rm -rf /tmp/gitea-certs diff --git a/PROJECT b/PROJECT index 2217e54..0242ff5 100644 --- a/PROJECT +++ b/PROJECT @@ -226,6 +226,8 @@ resources: version: v1beta3 webhooks: conversion: true + validation: true + mutation: true webhookVersion: v1 - api: crdVersion: v1 @@ -239,4 +241,17 @@ resources: conversion: true validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: io + group: syngit + kind: RemoteTarget + path: github.com/syngit-org/syngit/pkg/api/v1beta3 + version: v1beta3 + webhooks: + conversion: true + validation: true + webhookVersion: v1 version: "3" diff --git a/charts/0.4.0/templates/crd/syngit.io_remotesyncer.yaml b/charts/0.4.0/templates/crd/syngit.io_remotesyncer.yaml index 33aab2b..805ea97 100644 --- a/charts/0.4.0/templates/crd/syngit.io_remotesyncer.yaml +++ b/charts/0.4.0/templates/crd/syngit.io_remotesyncer.yaml @@ -691,8 +691,70 @@ spec: defaultBlockAppliedMessage: type: string defaultBranch: + default: main example: main type: string + defaultRemoteTargetRef: + description: |- + ObjectReference contains enough information to let you inspect or modify the referred object. + --- + New uses of this type are discouraged because of difficulty describing its usage when embedded in APIs. + 1. Ignored fields. It includes many fields which are not generally honored. For instance, ResourceVersion and FieldPath are both very rarely valid in actual usage. + 2. Invalid usage help. It is impossible to add specific help for individual usage. In most embedded usages, there are particular + restrictions like, "must refer only to types A and B" or "UID not honored" or "name must be restricted". + Those cannot be well described when embedded. + 3. Inconsistent validation. Because the usages are different, the validation rules are different by usage, which makes it hard for users to predict what will happen. + 4. The fields are both imprecise and overly precise. Kind is not a precise mapping to a URL. This can produce ambiguity + during interpretation and require a REST mapping. In most cases, the dependency is on the group,resource tuple + and the version of the actual struct is irrelevant. + 5. We cannot easily change it. Because this type is embedded in many locations, updates to this type + will affect numerous schemas. Don't make new APIs embed an underspecified API type they do not control. + + + Instead of using this type, create a locally provided and used type that is well-focused on your reference. + For example, ServiceReferences for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 . + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + TODO: this design is not final and this field is subject to change in the future. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic defaultRemoteUserRef: description: |- ObjectReference contains enough information to let you inspect or modify the referred object. @@ -831,6 +893,55 @@ spec: example: https://git.example.com/my-repo.git format: uri type: string + remoteTargetSelector: + description: |- + A label selector is a label query over a set of resources. The result of matchLabels and + matchExpressions are ANDed. An empty label selector matches all objects. A null + label selector matches no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic remoteUserBindingSelector: description: |- A label selector is a label query over a set of resources. The result of matchLabels and @@ -1018,13 +1129,13 @@ spec: - CommitApply type: string targetStrategy: - default: SameBranch + default: OneTarget enum: - - SameBranch - - MultipleBranch - - Fork + - OneTarget + - MultipleTarget type: string required: + - defaultBranch - defaultUnauthorizedUserMode - remoteRepository - scopedResources @@ -1203,11 +1314,15 @@ spec: - version type: object lastPushedObjectCommitHash: - type: string + items: + type: string + type: array lastPushedObjectGitPath: type: string lastPushedObjectGitRepo: - type: string + items: + type: string + type: array lastPushedObjectState: type: string lastPushedObjectTime: diff --git a/charts/0.4.0/templates/crd/syngit.io_remotetargets.yaml b/charts/0.4.0/templates/crd/syngit.io_remotetargets.yaml new file mode 100644 index 0000000..c44ab4e --- /dev/null +++ b/charts/0.4.0/templates/crd/syngit.io_remotetargets.yaml @@ -0,0 +1,174 @@ +{{- if eq .Values.crds.enabled true }} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + {{- if eq .Values.webhook.certmanager.enabled true }} + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/syngit-webhook-cert + {{- end }} + name: remotetargets.syngit.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: {{ .Release.Namespace }} + name: syngit-webhook-service + path: /convert + port: 443 + conversionReviewVersions: + - v1 + group: syngit.io + names: + categories: + - syngit + kind: RemoteTarget + listKind: RemoteTargetList + plural: remotetargets + shortNames: + - rt + - rts + singular: remotetarget + scope: Namespaced + versions: + - name: v1beta3 + schema: + openAPIV3Schema: + description: RemoteTarget is the Schema for the remotetargets 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: RemoteTargetSpec defines the desired state of RemoteTarget. + properties: + mergeStrategy: + enum: + - TryFastForwardOrDie + - TryFastForwardOrHardReset + - TryHardResetOrDie + - "" + type: string + targetBranch: + type: string + targetRepository: + example: https://git.example.com/my-target-repo.git + format: uri + type: string + upstreamBranch: + type: string + upstreamRepository: + example: https://git.example.com/my-upstream-repo.git + format: uri + type: string + required: + - targetBranch + - targetRepository + - upstreamBranch + - upstreamRepository + type: object + status: + description: RemoteTargetStatus defines the observed state of RemoteTarget. + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + lastConsistencyOperationTime: + type: string + lastConsistencyOperationType: + type: string + lastObservedCommitHash: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +{{- end }} \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index a8c0816..defcaf9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -178,6 +178,15 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "RemoteSyncer") os.Exit(1) } + + if err = (&controller.RemoteTargetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "RemoteTarget") + os.Exit(1) + } + // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { if err = webhooksyngitv1beta3.SetupRemoteUserWebhookWithManager(mgr); err != nil { @@ -192,6 +201,10 @@ func main() { setupLog.Error(err, "unable to create webhook", "webhook", "RemoteUserBinding") os.Exit(1) } + if err = webhooksyngitv1beta3.SetupRemoteTargetWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "RemoteTarget") + os.Exit(1) + } mgr.GetWebhookServer().Register("/syngit-v1beta3-remoteuser-association", &webhook.Admission{Handler: &webhooksyngitv1beta3.RemoteUserAssociationWebhookHandler{ Client: mgr.GetClient(), Decoder: admission.NewDecoder(mgr.GetScheme()), @@ -208,6 +221,10 @@ func main() { Client: mgr.GetClient(), Decoder: admission.NewDecoder(mgr.GetScheme()), }}) + mgr.GetWebhookServer().Register("/syngit-v1beta3-remotesyncer-target-pattern", &webhook.Admission{Handler: &webhooksyngitv1beta3.RemoteSyncerTargetPatternWebhookHandler{ + Client: mgr.GetClient(), + Decoder: admission.NewDecoder(mgr.GetScheme()), + }}) } //+kubebuilder:scaffold:builder diff --git a/config/crd/bases/syngit.io_remotesyncers.yaml b/config/crd/bases/syngit.io_remotesyncers.yaml index 3eb06e3..1daf0e9 100644 --- a/config/crd/bases/syngit.io_remotesyncers.yaml +++ b/config/crd/bases/syngit.io_remotesyncers.yaml @@ -676,8 +676,70 @@ spec: defaultBlockAppliedMessage: type: string defaultBranch: + default: main example: main type: string + defaultRemoteTargetRef: + description: |- + ObjectReference contains enough information to let you inspect or modify the referred object. + --- + New uses of this type are discouraged because of difficulty describing its usage when embedded in APIs. + 1. Ignored fields. It includes many fields which are not generally honored. For instance, ResourceVersion and FieldPath are both very rarely valid in actual usage. + 2. Invalid usage help. It is impossible to add specific help for individual usage. In most embedded usages, there are particular + restrictions like, "must refer only to types A and B" or "UID not honored" or "name must be restricted". + Those cannot be well described when embedded. + 3. Inconsistent validation. Because the usages are different, the validation rules are different by usage, which makes it hard for users to predict what will happen. + 4. The fields are both imprecise and overly precise. Kind is not a precise mapping to a URL. This can produce ambiguity + during interpretation and require a REST mapping. In most cases, the dependency is on the group,resource tuple + and the version of the actual struct is irrelevant. + 5. We cannot easily change it. Because this type is embedded in many locations, updates to this type + will affect numerous schemas. Don't make new APIs embed an underspecified API type they do not control. + + + Instead of using this type, create a locally provided and used type that is well-focused on your reference. + For example, ServiceReferences for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 . + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + TODO: this design is not final and this field is subject to change in the future. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic defaultRemoteUserRef: description: |- ObjectReference contains enough information to let you inspect or modify the referred object. @@ -816,6 +878,55 @@ spec: example: https://git.example.com/my-repo.git format: uri type: string + remoteTargetSelector: + description: |- + A label selector is a label query over a set of resources. The result of matchLabels and + matchExpressions are ANDed. An empty label selector matches all objects. A null + label selector matches no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic remoteUserBindingSelector: description: |- A label selector is a label query over a set of resources. The result of matchLabels and @@ -1003,13 +1114,13 @@ spec: - CommitApply type: string targetStrategy: - default: SameBranch + default: OneTarget enum: - - SameBranch - - MultipleBranch - - Fork + - OneTarget + - MultipleTarget type: string required: + - defaultBranch - defaultUnauthorizedUserMode - remoteRepository - scopedResources @@ -1188,11 +1299,15 @@ spec: - version type: object lastPushedObjectCommitHash: - type: string + items: + type: string + type: array lastPushedObjectGitPath: type: string lastPushedObjectGitRepo: - type: string + items: + type: string + type: array lastPushedObjectState: type: string lastPushedObjectTime: diff --git a/config/crd/bases/syngit.io_remotetargets.yaml b/config/crd/bases/syngit.io_remotetargets.yaml new file mode 100644 index 0000000..8d41ed3 --- /dev/null +++ b/config/crd/bases/syngit.io_remotetargets.yaml @@ -0,0 +1,158 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: remotetargets.syngit.io +spec: + group: syngit.io + names: + categories: + - syngit + kind: RemoteTarget + listKind: RemoteTargetList + plural: remotetargets + shortNames: + - rt + - rts + singular: remotetarget + scope: Namespaced + versions: + - name: v1beta3 + schema: + openAPIV3Schema: + description: RemoteTarget is the Schema for the remotetargets 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: RemoteTargetSpec defines the desired state of RemoteTarget. + properties: + mergeStrategy: + enum: + - TryFastForwardOrDie + - TryFastForwardOrHardReset + - TryHardResetOrDie + - "" + type: string + targetBranch: + type: string + targetRepository: + example: https://git.example.com/my-target-repo.git + format: uri + type: string + upstreamBranch: + type: string + upstreamRepository: + example: https://git.example.com/my-upstream-repo.git + format: uri + type: string + required: + - targetBranch + - targetRepository + - upstreamBranch + - upstreamRepository + type: object + status: + description: RemoteTargetStatus defines the observed state of RemoteTarget. + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + lastConsistencyOperationTime: + type: string + lastConsistencyOperationType: + type: string + lastObservedCommitHash: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 47b2399..67ddb97 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -5,6 +5,7 @@ resources: - bases/syngit.io_remoteusers.yaml - bases/syngit.io_remoteuserbindings.yaml - bases/syngit.io_remotesyncers.yaml +- bases/syngit.io_remotetargets.yaml #+kubebuilder:scaffold:crdkustomizeresource patches: @@ -13,6 +14,7 @@ patches: - path: patches/webhook_in_remotesyncers.yaml - path: patches/webhook_in_remoteuserbindings.yaml - path: patches/webhook_in_remoteusers.yaml +- path: patches/webhook_in_remotetargets.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -20,10 +22,11 @@ patches: #- path: patches/cainjection_in_remoteusers.yaml #- path: patches/cainjection_in_remotesyncers.yaml #- path: patches/cainjection_in_remoteuserbindings.yaml +#- path: patches/cainjection_in_remotetargets.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # [WEBHOOK] To enable webhook, uncomment the following section # the following config is for teaching kustomize how to do kustomization for CRDs. configurations: -- kustomizeconfig.yaml \ No newline at end of file +- kustomizeconfig.yaml diff --git a/config/crd/patches/cainjection_in_remotetargets.yaml b/config/crd/patches/cainjection_in_remotetargets.yaml new file mode 100644 index 0000000..d63999e --- /dev/null +++ b/config/crd/patches/cainjection_in_remotetargets.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: remotetargets.syngit.syngit.io diff --git a/config/crd/patches/webhook_in_remotetargets.yaml b/config/crd/patches/webhook_in_remotetargets.yaml new file mode 100644 index 0000000..48a52f3 --- /dev/null +++ b/config/crd/patches/webhook_in_remotetargets.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: remotetargets.syngit.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index f1af8a2..2d3a7c4 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -20,6 +20,8 @@ resources: # default, aiding admins in cluster management. Those roles are # not used by the Project itself. You can comment the following lines # if you do not want those helpers be installed with your Project. +- remotetarget_editor_role.yaml +- remotetarget_viewer_role.yaml - remotesyncer_editor_role.yaml - remotesyncer_viewer_role.yaml - remoteuserbinding_editor_role.yaml diff --git a/config/rbac/remotetarget_editor_role.yaml b/config/rbac/remotetarget_editor_role.yaml new file mode 100644 index 0000000..c4cde2a --- /dev/null +++ b/config/rbac/remotetarget_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit remotetargets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: syngit + app.kubernetes.io/managed-by: kustomize + name: remotetarget-editor-role +rules: +- apiGroups: + - syngit.io + resources: + - remotetargets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - syngit.io + resources: + - remotetargets/status + verbs: + - get diff --git a/config/rbac/remotetarget_viewer_role.yaml b/config/rbac/remotetarget_viewer_role.yaml new file mode 100644 index 0000000..a643532 --- /dev/null +++ b/config/rbac/remotetarget_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view remotetargets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: syngit + app.kubernetes.io/managed-by: kustomize + name: remotetarget-viewer-role +rules: +- apiGroups: + - syngit.io + resources: + - remotetargets + verbs: + - get + - list + - watch +- apiGroups: + - syngit.io + resources: + - remotetargets/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 3cfc2e9..ba61e80 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -73,6 +73,32 @@ rules: - get - patch - update +- apiGroups: + - syngit.io + resources: + - remotetargets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - syngit.io + resources: + - remotetargets/finalizers + verbs: + - update +- apiGroups: + - syngit.io + resources: + - remotetargets/status + verbs: + - get + - patch + - update - apiGroups: - syngit.io resources: diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 322fd18..3bc443a 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -9,4 +9,5 @@ resources: - syngit_v1beta3_remoteuser.yaml - syngit_v1beta3_remoteuserbinding.yaml - syngit_v1beta3_remotesyncer.yaml +- syngit_v1beta3_remotetarget.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/syngit_v1beta3_remotetarget.yaml b/config/samples/syngit_v1beta3_remotetarget.yaml new file mode 100644 index 0000000..3b38eb4 --- /dev/null +++ b/config/samples/syngit_v1beta3_remotetarget.yaml @@ -0,0 +1,9 @@ +apiVersion: syngit.io/v1beta3 +kind: RemoteTarget +metadata: + labels: + app.kubernetes.io/name: syngit + app.kubernetes.io/managed-by: kustomize + name: remotetarget-sample +spec: + # TODO(user): Add fields here diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 431e36c..cf78f66 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -45,6 +45,47 @@ webhooks: resources: - remotesyncers sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /syngit-v1beta3-remotesyncer-target-pattern + failurePolicy: Fail + name: vremotesyncers-target-pattern.v1beta3.syngit.io + rules: + - apiGroups: + - syngit.io + apiVersions: + - v1beta3 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - remotesyncers + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-syngit-io-v1beta3-remotetarget + failurePolicy: Fail + name: vremotetarget-v1beta3.kb.io + rules: + - apiGroups: + - syngit.io + apiVersions: + - v1beta3 + operations: + - CREATE + - UPDATE + resources: + - remotetargets + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -65,6 +106,26 @@ webhooks: resources: - remoteusers sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-syngit-io-v1beta3-remoteuserbinding + failurePolicy: Fail + name: vremoteuserbinding-v1beta3.kb.io + rules: + - apiGroups: + - syngit.io + apiVersions: + - v1beta3 + operations: + - CREATE + - UPDATE + resources: + - remoteuserbindings + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/go.mod b/go.mod index d80d9aa..e8ef770 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.22.2 require ( github.com/go-git/go-billy/v5 v5.6.0 + github.com/go-git/go-git v4.7.0+incompatible github.com/go-git/go-git/v5 v5.13.0 github.com/gorilla/mux v1.8.1 github.com/joho/godotenv v1.5.1 @@ -104,6 +105,7 @@ require ( google.golang.org/grpc v1.58.3 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/src-d/go-git.v4 v4.13.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect k8s.io/apiextensions-apiserver v0.29.0 // indirect k8s.io/apiserver v0.29.0 // indirect diff --git a/go.sum b/go.sum index 5252da2..eca4ddc 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,9 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= @@ -21,6 +24,7 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= @@ -31,6 +35,7 @@ github.com/elazarl/goproxy v1.2.1 h1:njjgvO6cRG9rIqN2ebkqy6cQz2Njkx7Fsfv/zIZqgug github.com/elazarl/goproxy v1.2.1/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= @@ -39,14 +44,18 @@ github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1 github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= +github.com/go-git/go-git v4.7.0+incompatible h1:+W9rgGY4DOKKdX2x6HxSR7HNeTxqiKrOvKnuittYVdA= +github.com/go-git/go-git v4.7.0+incompatible/go.mod h1:6+421e08gnZWn30y26Vchf7efgYLe4dl5OQbBSUXShE= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.13.0 h1:vLn5wlGIh/X78El6r3Jr+30W16Blk0CTcxTYcYPWi5E= @@ -79,6 +88,7 @@ github.com/google/cel-go v0.17.7 h1:6ebJFzu1xO2n7TLtN+UBqShGBhlD85bhvglh5DpcfqQ= github.com/google/cel-go v0.17.7/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -97,12 +107,14 @@ github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -112,6 +124,7 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -119,6 +132,7 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -130,8 +144,10 @@ github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -146,6 +162,7 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -153,9 +170,12 @@ github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0 github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= +github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -167,6 +187,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= 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= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -193,7 +214,9 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -208,6 +231,7 @@ golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -221,7 +245,9 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -243,6 +269,7 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -267,11 +294,18 @@ google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSs google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= +gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/controller/remotesyncer_controller_test.go b/internal/controller/remotesyncer_controller_test.go index 5c60883..1d902df 100644 --- a/internal/controller/remotesyncer_controller_test.go +++ b/internal/controller/remotesyncer_controller_test.go @@ -64,6 +64,9 @@ var _ = Describe("RemoteSyncer Controller", func() { ObjectMeta: metav1.ObjectMeta{ Name: remotesyncername, Namespace: userNamespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: "main", + }, }, Spec: syngit.RemoteSyncerSpec{ DefaultBlockAppliedMessage: "test", @@ -71,7 +74,7 @@ var _ = Describe("RemoteSyncer Controller", func() { DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitOnly, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: "https://dummy-git-server.com", ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ diff --git a/internal/controller/remotetarget_controller.go b/internal/controller/remotetarget_controller.go new file mode 100644 index 0000000..2950736 --- /dev/null +++ b/internal/controller/remotetarget_controller.go @@ -0,0 +1,56 @@ +/* +Copyright 2024. + +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 controller + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + syngitv1beta3 "github.com/syngit-org/syngit/pkg/api/v1beta3" +) + +// RemoteTargetReconciler reconciles a RemoteTarget object +type RemoteTargetReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=syngit.io,resources=remotetargets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=syngit.io,resources=remotetargets/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=syngit.io,resources=remotetargets/finalizers,verbs=update + +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/reconcile +func (r *RemoteTargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + // TODO(user): your logic here + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *RemoteTargetReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&syngitv1beta3.RemoteTarget{}). + Named("remotetarget"). + Complete(r) +} diff --git a/internal/controller/remotetarget_controller_test.go b/internal/controller/remotetarget_controller_test.go new file mode 100644 index 0000000..92d3087 --- /dev/null +++ b/internal/controller/remotetarget_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2024. + +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 controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + syngitv1beta3 "github.com/syngit-org/syngit/pkg/api/v1beta3" +) + +var _ = Describe("RemoteTarget Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + remotetarget := &syngitv1beta3.RemoteTarget{} + + BeforeEach(func() { + By("creating the custom resource for the Kind RemoteTarget") + err := k8sClient.Get(ctx, typeNamespacedName, remotetarget) + if err != nil && errors.IsNotFound(err) { + resource := &syngitv1beta3.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &syngitv1beta3.RemoteTarget{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance RemoteTarget") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &RemoteTargetReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/internal/interceptor/git_consistency.go b/internal/interceptor/git_consistency.go new file mode 100644 index 0000000..fd58523 --- /dev/null +++ b/internal/interceptor/git_consistency.go @@ -0,0 +1,363 @@ +package interceptor + +import ( + "bytes" + "errors" + "fmt" + "io" + "strings" + + "github.com/go-git/go-billy/v5/memfs" + git "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/go-git/go-git/v5/storage/memory" + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" +) + +type GitConsistency struct { + strategy syngit.MergeStrategy + targetRepository *git.Repository + upstreamRepository *git.Repository +} + +const ( + originRemote = "origin" + upstreamRemote = "upstream" +) + +func getRepository(gp GitPusher, repo string, branch string) (*git.Repository, error) { + // Clone the repository into memory + var verboseOutput bytes.Buffer + cloneOptions := &git.CloneOptions{ + URL: repo, + ReferenceName: plumbing.ReferenceName(branch), + Auth: &http.BasicAuth{ + Username: gp.gitUser, + Password: gp.gitToken, + }, + SingleBranch: true, + InsecureSkipTLS: gp.remoteSyncer.Spec.InsecureSkipTlsVerify, + Progress: io.MultiWriter(&verboseOutput), + } + if gp.caBundle != nil { + cloneOptions.CABundle = gp.caBundle + } + repository, err := git.Clone(memory.NewStorage(), memfs.New(), cloneOptions) + if err != nil { + variables := fmt.Sprintf("\nRepository: %s\nReference: %s\nUsername: %s\nEmail: %s\n", + repo, + plumbing.ReferenceName(branch), + gp.gitUser, + gp.gitEmail, + ) + errMsg := fmt.Sprintf("failed to clone repository: %s\nVerbose output: %s\nVariables: %s\n", err.Error(), verboseOutput.String(), variables) + return nil, errors.New(errMsg) + } + + return repository, nil +} + +func GetUpstreamRepository(gp GitPusher) (*git.Repository, error) { + return getRepository(gp, gp.remoteTarget.Spec.UpstreamRepository, gp.remoteTarget.Spec.UpstreamBranch) +} + +func GetTargetRepository(gp GitPusher) (*git.Repository, error) { + return getRepository(gp, gp.remoteTarget.Spec.TargetRepository, gp.remoteTarget.Spec.UpstreamBranch) +} + +func (gc GitConsistency) GetWorkTree(gp GitPusher) (*git.Worktree, bool, error) { + + if gc.strategy == syngit.TryFastForwardOrHardReset { + wt, err := gc.upstreamBasedPull(gp) + if err != nil { + wt, err = gc.upstreamBasedHardReset(gp) + if err != nil { + return nil, false, err + } + return wt, true, nil + } + return wt, false, nil + } + + if gc.strategy == syngit.TryHardResetOrDie { + wt, err := gc.upstreamBasedHardReset(gp) + if err != nil { + return nil, false, err + } + return wt, true, nil + } + + if gc.strategy == syngit.TryFastForwardOrDie { + wt, err := gc.upstreamBasedPull(gp) + if err != nil { + return nil, false, err + } + return wt, false, nil + } + + return nil, false, fmt.Errorf("wrong target strategy; got %s", gc.strategy) +} + +func (gc GitConsistency) upstreamBasedHardReset(gp GitPusher) (*git.Worktree, error) { + + targetBranch := gp.remoteTarget.Spec.TargetBranch + targetBranchRef := plumbing.NewBranchReferenceName(targetBranch) + upstreamRemoteRef := plumbing.ReferenceName(fmt.Sprintf("refs/remotes/%s/%s", upstreamRemote, gp.remoteSyncer.Spec.DefaultBranch)) + + remErr := gc.fetchUpstream(gp) + if remErr != nil { + return nil, remErr + } + + worktree, err := gc.targetRepository.Worktree() + if err != nil { + return nil, fmt.Errorf("failed to get worktree: %w", err) + } + + upstreamLastCommitRef, err := gc.targetRepository.Reference(upstreamRemoteRef, true) + if err != nil { + return nil, fmt.Errorf("failed to find remote reference %s: %w", upstreamRemoteRef.String(), err) + } + err = worktree.Checkout(&git.CheckoutOptions{ + Hash: upstreamLastCommitRef.Hash(), + }) + if err != nil { + return nil, fmt.Errorf("failed to checkout upstream commit: %w", err) + } + + err = gc.targetRepository.Storer.SetReference(plumbing.NewHashReference(targetBranchRef, upstreamLastCommitRef.Hash())) + if err != nil { + return nil, fmt.Errorf("failed to create local branch %s: %w", targetBranchRef.String(), err) + } + + err = gc.checkoutToBranch(worktree, targetBranch) + if err != nil { + return nil, err + } + + if err := worktree.Reset(&git.ResetOptions{ + Commit: upstreamLastCommitRef.Hash(), + Mode: git.HardReset, + }); err != nil { + return nil, fmt.Errorf("failed to hard reset: %w", err) + } + + return worktree, nil +} + +func (gc GitConsistency) upstreamBasedPull(gp GitPusher) (*git.Worktree, error) { + + targetBranch := gp.remoteTarget.Spec.TargetBranch + + remErr := gc.fetchUpstream(gp) + if remErr != nil { + return nil, remErr + } + + targetWorktree, err := gc.targetRepository.Worktree() + if err != nil { + return nil, fmt.Errorf("failed to get worktree for target repository: %w", err) + } + + err = gc.checkoutToBranch(targetWorktree, targetBranch) + if err != nil { + return nil, err + } + + var verboseOutput bytes.Buffer + pullOptions := &git.PullOptions{ + RemoteName: upstreamRemote, + ReferenceName: plumbing.NewBranchReferenceName("main"), + // ReferenceName: upstreamRemoteRef, + SingleBranch: true, + Auth: &http.BasicAuth{ + Username: gp.gitUser, + Password: gp.gitToken, + }, + InsecureSkipTLS: gp.remoteSyncer.Spec.InsecureSkipTlsVerify, + Progress: io.MultiWriter(&verboseOutput), + } + if gp.caBundle != nil { + pullOptions.CABundle = gp.caBundle + } + err = targetWorktree.Pull(pullOptions) + if err != nil && err != git.NoErrAlreadyUpToDate { + variables := fmt.Sprintf("\nRemote: %s\nUpstream ref: %s\nReference: %s\nUsername: %s\nEmail: %s\n", + upstreamRemote, + plumbing.HEAD, + targetBranch, + gp.gitUser, + gp.gitEmail, + ) + errMsg := fmt.Sprintf("failed to pull remote: %s\nVerbose output: %s\nVariables: %s\n", err.Error(), verboseOutput.String(), variables) + return nil, errors.New(errMsg) + } + + pullOptions = &git.PullOptions{ + RemoteName: originRemote, + ReferenceName: plumbing.NewBranchReferenceName(targetBranch), + // ReferenceName: upstreamRemoteRef, + SingleBranch: true, + Auth: &http.BasicAuth{ + Username: gp.gitUser, + Password: gp.gitToken, + }, + InsecureSkipTLS: gp.remoteSyncer.Spec.InsecureSkipTlsVerify, + Progress: io.MultiWriter(&verboseOutput), + } + if gp.caBundle != nil { + pullOptions.CABundle = gp.caBundle + } + err = targetWorktree.Pull(pullOptions) + if err != nil && err != git.NoErrAlreadyUpToDate && !strings.Contains(err.Error(), "reference not found") { + variables := fmt.Sprintf("\nRemote: %s\nUpstream ref: %s\nReference: %s\nUsername: %s\nEmail: %s\n", + upstreamRemote, + plumbing.HEAD, + targetBranch, + gp.gitUser, + gp.gitEmail, + ) + errMsg := fmt.Sprintf("failed to pull target remote: %s\nVerbose output: %s\nVariables: %s\n", err.Error(), verboseOutput.String(), variables) + return nil, errors.New(errMsg) + } + + targetRef, err := gc.targetRepository.Reference(plumbing.NewBranchReferenceName(targetBranch), true) + if err != nil { + return nil, fmt.Errorf("failed to get target branch reference: %w", err) + } + + err = gc.targetRepository.Storer.SetReference(targetRef) + if err != nil { + return nil, fmt.Errorf("failed to set target branch %s: %w", plumbing.NewBranchReferenceName(targetBranch).String(), err) + } + + // Merge the main branch into the target branch + mainRef, err := gc.targetRepository.Reference(plumbing.NewBranchReferenceName("main"), true) + if err != nil { + return nil, fmt.Errorf("failed to get main branch reference: %w", err) + } + + // Check if the target branch already contains the commit from the main branch + mainCommitHash := mainRef.Hash() + contains, err := gc.branchContainsCommit(*targetRef, mainCommitHash) + if err != nil { + return nil, fmt.Errorf("failed to check if target branch contains commit: %w", err) + } + if contains { + // Target branch already contains the main branch commit. Skipping merge. + return targetWorktree, nil + } + + mergeOptions := &git.MergeOptions{ + Strategy: git.FastForwardMerge, + } + mergeErr := gc.targetRepository.Merge(*mainRef, *mergeOptions) + if mergeErr != nil { + return nil, fmt.Errorf("failed to merge the %s reference in the current branch", mainRef.String()) + } + + // Return the updated worktree + updatedWorktree, err := gc.targetRepository.Worktree() + if err != nil { + return nil, fmt.Errorf("failed to get updated worktree: %w", err) + } + + return updatedWorktree, nil +} + +func (gc GitConsistency) branchContainsCommit(branchRef plumbing.Reference, commitHash plumbing.Hash) (bool, error) { + branchIter, err := gc.targetRepository.Log(&git.LogOptions{From: branchRef.Hash()}) + if err != nil { + return false, fmt.Errorf("failed to get branch log: %w", err) + } + defer branchIter.Close() + + for { + commit, err := branchIter.Next() + if err != nil { + break + } + if commit.Hash == commitHash { + return true, nil + } + } + + return false, nil +} + +func (gc GitConsistency) checkoutToBranch(worktree *git.Worktree, targetBranch string) error { + localRef := plumbing.NewBranchReferenceName(targetBranch) + _, err := gc.targetRepository.Reference(localRef, true) + if err == plumbing.ErrReferenceNotFound { + + err = worktree.Checkout(&git.CheckoutOptions{ + Branch: localRef, + Create: true, + }) + if err != nil { + return fmt.Errorf("failed to create and checkout branch %s: %w", targetBranch, err) + } + } else if err != nil { + return fmt.Errorf("failed to check branch reference: %w", err) + } + + err = worktree.Checkout(&git.CheckoutOptions{ + Branch: localRef, + }) + if err != nil { + return fmt.Errorf("failed to checkout branch %s: %w", targetBranch, err) + } + + return nil +} + +func (gc GitConsistency) fetchUpstream(gp GitPusher) error { + + upstreamURL := gp.remoteSyncer.Spec.RemoteRepository + + if _, remErr := gc.targetRepository.Remote(upstreamRemote); remErr == git.ErrRemoteNotFound { + _, err := gc.targetRepository.CreateRemote(&config.RemoteConfig{ + Name: upstreamRemote, + URLs: []string{upstreamURL}, + }) + if err != nil { + return fmt.Errorf("failed to create upstream remote: %w", err) + } + } else if remErr != nil { + return fmt.Errorf("failed to get upstream remote: %w", remErr) + } + + var verboseOutput bytes.Buffer + fetchOptions := &git.FetchOptions{ + RemoteName: upstreamRemote, + RemoteURL: upstreamURL, + Auth: &http.BasicAuth{ + Username: gp.gitUser, + Password: gp.gitToken, + }, + RefSpecs: []config.RefSpec{ + config.RefSpec("+refs/heads/*:refs/remotes/origin/*"), + config.RefSpec("+refs/heads/*:refs/remotes/upstream/*"), + }, + InsecureSkipTLS: gp.remoteSyncer.Spec.InsecureSkipTlsVerify, + Progress: io.MultiWriter(&verboseOutput), + } + if gp.caBundle != nil { + fetchOptions.CABundle = gp.caBundle + } + + err := gc.targetRepository.Fetch(fetchOptions) + if err != nil && err != git.NoErrAlreadyUpToDate { + variables := fmt.Sprintf("\nRepository: %s\nUsername: %s\nEmail: %s\n", + upstreamURL, + gp.gitUser, + gp.gitEmail, + ) + errMsg := fmt.Sprintf("failed to fetch remote: %s\nVerbose output: %s\nVariables: %s\n", err.Error(), verboseOutput.String(), variables) + return errors.New(errMsg) + } + + return nil +} diff --git a/internal/interceptor/git_pusher.go b/internal/interceptor/git_pusher.go index a9435d6..b5316bd 100644 --- a/internal/interceptor/git_pusher.go +++ b/internal/interceptor/git_pusher.go @@ -10,12 +10,11 @@ import ( "strings" "time" - "github.com/go-git/go-billy/v5/memfs" git "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/transport/http" - "github.com/go-git/go-git/v5/storage/memory" admissionv1 "k8s.io/api/admission/v1" syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" @@ -23,61 +22,74 @@ import ( ) type GitPusher struct { - remoteSyncer syngit.RemoteSyncer - interceptedYAML string - interceptedGVR schema.GroupVersionResource - interceptedName string - branch string - gitUser string - gitEmail string - gitToken string - operation admissionv1.Operation - insecureSkipTlsVerify bool - caBundle []byte + remoteSyncer syngit.RemoteSyncer + remoteTarget syngit.RemoteTarget + interceptedYAML string + interceptedGVR schema.GroupVersionResource + interceptedName string + gitUser string + gitEmail string + gitToken string + operation admissionv1.Operation + caBundle []byte } type GitPushResponse struct { path string // The git path were the resource has been pushed commitHash string // The commit hash of the commit + url string // The url of the repository } +var forcePush bool + func (gp *GitPusher) Push() (GitPushResponse, error) { - gpResponse := &GitPushResponse{path: "", commitHash: ""} - gp.branch = gp.remoteSyncer.Spec.DefaultBranch + gpResponse := &GitPushResponse{path: "", commitHash: "", url: gp.remoteTarget.Spec.TargetRepository} + + var w *git.Worktree + var repo *git.Repository + + if gp.remoteTarget.Spec.MergeStrategy == "" { + // Same repo & branch between target and upstream + // PRE-STEP 1 : Get the repo + var getRepoErr error + repo, getRepoErr = GetTargetRepository(*gp) + if getRepoErr != nil { + return *gpResponse, getRepoErr + } + // PRE-STEP 2 : Get the worktree + var err error + w, err = repo.Worktree() + if err != nil { + errMsg := "failed to get worktree: " + err.Error() + return *gpResponse, errors.New(errMsg) + } - // Clone the repository into memory - var verboseOutput bytes.Buffer - cloneOption := &git.CloneOptions{ - URL: gp.remoteSyncer.Spec.RemoteRepository, - ReferenceName: plumbing.ReferenceName(gp.branch), - Auth: &http.BasicAuth{ - Username: gp.gitUser, - Password: gp.gitToken, - }, - SingleBranch: true, - InsecureSkipTLS: gp.insecureSkipTlsVerify, - Progress: io.MultiWriter(&verboseOutput), - } - if gp.caBundle != nil { - cloneOption.CABundle = gp.caBundle - } - repo, err := git.Clone(memory.NewStorage(), memfs.New(), cloneOption) - if err != nil { - variables := fmt.Sprintf("\nRepository: %s\nReference: %s\nUsername: %s\nEmail: %s\n", - gp.remoteSyncer.Spec.RemoteRepository, - plumbing.ReferenceName(gp.branch), - gp.gitUser, - gp.gitEmail, - ) - errMsg := fmt.Sprintf("failed to clone repository: %s\nVerbose output: %s\nVariables: %s\n", err.Error(), verboseOutput.String(), variables) - return *gpResponse, errors.New(errMsg) - } + forcePush = false - // Get the working directory for the repository - w, err := repo.Worktree() - if err != nil { - errMsg := "failed to get worktree: " + err.Error() - return *gpResponse, errors.New(errMsg) + } else { + // Different target and upstream + // PRE-STEP 1 : Get the repos + upstreamRepo, getRepoErr := GetUpstreamRepository(*gp) + if getRepoErr != nil { + return *gpResponse, getRepoErr + } + repo, getRepoErr = GetTargetRepository(*gp) + if getRepoErr != nil { + return *gpResponse, getRepoErr + } + + // PRE-STEP 2 : Get the worktree + gc := GitConsistency{ + upstreamRepository: upstreamRepo, + targetRepository: repo, + strategy: gp.remoteTarget.Spec.MergeStrategy, + } + var err error + w, forcePush, err = gc.GetWorkTree(*gp) + if err != nil { + errMsg := "failed to get worktree: " + err.Error() + return *gpResponse, errors.New(errMsg) + } } // STEP 1 : Set the path @@ -245,7 +257,7 @@ func (gp *GitPusher) commitChanges(w *git.Worktree, pathToAdd string) (string, e }, }) if err != nil { - errMsg := "failed to commit changes: " + err.Error() + errMsg := fmt.Sprintf("failed to commit changes (%s - %s): %s", gp.remoteTarget.Spec.TargetRepository, gp.remoteTarget.Spec.TargetBranch, err.Error()) return "", errors.New(errMsg) } @@ -253,20 +265,26 @@ func (gp *GitPusher) commitChanges(w *git.Worktree, pathToAdd string) (string, e } func (gp *GitPusher) pushChanges(repo *git.Repository) error { + targetBranch := gp.remoteTarget.Spec.TargetBranch + variables := fmt.Sprintf("\nRepository: %s\nReference: %s\nUsername: %s\nEmail: %s\n", gp.remoteSyncer.Spec.RemoteRepository, - plumbing.ReferenceName(gp.branch), + plumbing.ReferenceName(targetBranch), gp.gitUser, gp.gitEmail, ) var verboseOutput bytes.Buffer pushOptions := &git.PushOptions{ + RefSpecs: []config.RefSpec{ + config.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%s", targetBranch, targetBranch)), + }, Auth: &http.BasicAuth{ Username: gp.gitUser, Password: gp.gitToken, }, - InsecureSkipTLS: gp.insecureSkipTlsVerify, + InsecureSkipTLS: gp.remoteSyncer.Spec.InsecureSkipTlsVerify, Progress: io.MultiWriter(&verboseOutput), // Capture verbose output + Force: forcePush, } if gp.caBundle != nil { pushOptions.CABundle = gp.caBundle diff --git a/internal/interceptor/webhook_request_checker.go b/internal/interceptor/webhook_request_checker.go index a0fcbfb..160886b 100644 --- a/internal/interceptor/webhook_request_checker.go +++ b/internal/interceptor/webhook_request_checker.go @@ -4,10 +4,13 @@ import ( "context" "encoding/json" "errors" + "fmt" "net/url" + "slices" "strings" "sync" + patterns "github.com/syngit-org/syngit/internal/patterns/v1beta3" syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" "github.com/syngit-org/syngit/pkg/utils" "gopkg.in/yaml.v3" @@ -42,19 +45,22 @@ type wrcDetails struct { messageAddition string // GitPusher information - repoUrl string - repoPath string - repoHost string - commitHash string - gitUser gitUser - insecureSkipTlsVerify bool - caBundle []byte - pushDetails string + serverHost string + caBundle []byte + gitUser gitUser + targetsPushInformation []pushInformation + pushDetails string // Error errorDuringProcess bool } +type pushInformation struct { + repoUrl string + repoPath string + commitHash string +} + const ( defaultFailureMessage = "The changes have not been pushed to the remote git repository:" defaultSuccessMessage = "The changes were correctly been pushed on the remote git repository." @@ -66,6 +72,8 @@ type WebhookRequestChecker struct { admReview admissionv1.AdmissionReview // The resources interceptor object remoteSyncer syngit.RemoteSyncer + // Targets + remoteTargets []syngit.RemoteTarget // The kubernetes client to make request to the api k8sClient client.Client // The manager where syngit is installed @@ -76,6 +84,8 @@ type WebhookRequestChecker struct { func (wrc *WebhookRequestChecker) ProcessSteps() admissionv1.AdmissionReview { + wrc.remoteTargets = []syngit.RemoteTarget{} + // STEP 1 : Get the request details rDetails, err := wrc.retrieveRequestDetails() if err != nil { @@ -120,8 +130,8 @@ func (wrc *WebhookRequestChecker) ProcessSteps() admissionv1.AdmissionReview { } // STEP 6 : Git push - isPushed, err := wrc.gitPush(&rDetails) - wrc.gitPushPostChecker(isPushed, err, &rDetails) + areTheyPushed, err := wrc.gitPush(&rDetails) + wrc.gitPushPostChecker(areTheyPushed, err, &rDetails) if err != nil { rDetails.errorDuringProcess = true return wrc.responseConstructor(rDetails) @@ -153,21 +163,12 @@ func (wrc *WebhookRequestChecker) retrieveRequestDetails() (wrcDetails, error) { wrc.updateStatusState("LastObservedObjectState", *details) + details.targetsPushInformation = []pushInformation{} + return *details, nil } -func (wrc *WebhookRequestChecker) userAllowed(details *wrcDetails) (bool, error) { - // Check if the user can push (and so, create the resource) - incomingUser := wrc.admReview.Request.UserInfo - u, err := url.Parse(wrc.remoteSyncer.Spec.RemoteRepository) - if err != nil { - errMsg := "error parsing git repository URL: " + err.Error() - details.messageAddition = errMsg - return false, errors.New(errMsg) - } - - fqdn := u.Host - details.repoHost = fqdn +func (wrc *WebhookRequestChecker) getRemoteUserBinding(username string, fqdn string, details *wrcDetails) (*syngit.RemoteUserBinding, *gitUser, error) { ctx := context.Background() gitUser := &gitUser{ gitUser: "", @@ -184,35 +185,154 @@ func (wrc *WebhookRequestChecker) userAllowed(details *wrcDetails) (bool, error) if labelErr != nil { errMsg := "error parsing the LabelSelector for the remoteUserBindingSelector: " + labelErr.Error() details.messageAddition = errMsg - return false, errors.New(errMsg) + return nil, nil, errors.New(errMsg) } listOps.LabelSelector = labelSelector } - err = wrc.k8sClient.List(ctx, remoteUserBindings, listOps) + err := wrc.k8sClient.List(ctx, remoteUserBindings, listOps) if err != nil { errMsg := err.Error() details.messageAddition = errMsg - return false, errors.New(errMsg) + return nil, nil, errors.New(errMsg) } + var rub = syngit.RemoteUserBinding{} userCountLoop := 0 // Prevent non-unique name attack for _, remoteUserBinding := range remoteUserBindings.Items { // The subject name can not be unique -> in specific conditions, a commit can be done as another user // Need to be studied - if remoteUserBinding.Spec.Subject.Name == incomingUser.Username { + if remoteUserBinding.Spec.Subject.Name == username { + gitUser, err = wrc.searchForGitTokenFromRemoteUserBinding(remoteUserBinding, fqdn) if err != nil { errMsg := err.Error() details.messageAddition = errMsg - return false, err + return nil, nil, err } userCountLoop++ + + rub = remoteUserBinding + } } + if userCountLoop > 1 { + const errMsg = "multiple RemoteUserBinding found OR the name of the user is not unique; this version of the operator work with the name as unique identifier for users" + details.messageAddition = errMsg + return nil, nil, errors.New(errMsg) + } + if userCountLoop == 0 { + return nil, gitUser, nil + } + + remoteUserBinding := &syngit.RemoteUserBinding{} + getErr := wrc.k8sClient.Get(ctx, types.NamespacedName{Name: rub.Name, Namespace: rub.Namespace}, remoteUserBinding) + if getErr != nil { + details.messageAddition = getErr.Error() + return nil, nil, getErr + } + + return remoteUserBinding, gitUser, nil +} + +func (wrc *WebhookRequestChecker) userAllowed(details *wrcDetails) (bool, error) { + // Check if the user can push (and so, create the resource) + incomingUser := wrc.admReview.Request.UserInfo + u, err := url.Parse(wrc.remoteSyncer.Spec.RemoteRepository) + if err != nil { + errMsg := "error parsing git repository URL: " + err.Error() + details.messageAddition = errMsg + return false, errors.New(errMsg) + } + + fqdn := u.Host + details.serverHost = fqdn + ctx := context.Background() + + var gitUser *gitUser + remoteUserBinding, gitUser, rubErr := wrc.getRemoteUserBinding(incomingUser.Username, fqdn, details) + if rubErr != nil { + return false, rubErr + } + + if remoteUserBinding != nil { + + // Trigger user specific pattern + // If annotation is set: + // Create the user specific remote target -> will create with the one-user-one-branch pattern by default. + // The external providers need to overwrite the target-repo & target-branch if the pattern is set to one-user-one-fork. + remoteTargetPattern := &patterns.UserSpecificPattern{ + PatternSpecification: patterns.PatternSpecification{ + Client: wrc.k8sClient, + NamespacedName: types.NamespacedName{Name: wrc.remoteSyncer.Name, Namespace: wrc.remoteSyncer.Namespace}, + }, + Username: incomingUser.Username, + RemoteSyncer: wrc.remoteSyncer, + } + err := patterns.Trigger(remoteTargetPattern, ctx) + if err != nil { + return false, err + } + if wrc.remoteSyncer.Annotations[syngit.RtAnnotationUserSpecificKey] != "" { + // The user specific pattern add a new association to the RemoteUserBinding. + // Therefore, we must either get again the new RemoteUserBinding OR + // add the user specific RemoteTarget to the current object. + userSpecificRemoteTarget := remoteTargetPattern.GetRemoteTarget() + remoteUserBinding.Spec.RemoteTargetRefs = append(remoteUserBinding.Spec.RemoteTargetRefs, corev1.ObjectReference{ + Name: userSpecificRemoteTarget.Name, + }) + } + + remoteTargetRefNames := []string{} + for _, remoteTargetRef := range remoteUserBinding.Spec.RemoteTargetRefs { + remoteTargetRefNames = append(remoteTargetRefNames, remoteTargetRef.Name) + } + + // Search for RemoteTargets + var remoteTargets = &syngit.RemoteTargetList{} + listOps := &client.ListOptions{ + Namespace: wrc.remoteSyncer.Namespace, + } + if wrc.remoteSyncer.Spec.RemoteTargetSelector != nil { + labelSelector, labelErr := v1.LabelSelectorAsSelector(wrc.remoteSyncer.Spec.RemoteTargetSelector) + if labelErr != nil { + errMsg := "error parsing the LabelSelector for the remoteTargetSelector: " + labelErr.Error() + details.messageAddition = errMsg + return false, errors.New(errMsg) + } + listOps.LabelSelector = labelSelector + } + listErr := wrc.k8sClient.List(ctx, remoteTargets, listOps) + + if listErr != nil { + details.messageAddition = listErr.Error() + return false, listErr + } + for _, remoteTarget := range remoteTargets.Items { + if slices.Contains(remoteTargetRefNames, remoteTarget.Name) { + if remoteTarget.Spec.UpstreamRepository == wrc.remoteSyncer.Spec.RemoteRepository && remoteTarget.Spec.UpstreamBranch == wrc.remoteSyncer.Spec.DefaultBranch { + wrc.remoteTargets = append(wrc.remoteTargets, remoteTarget) + } + } + } + + if wrc.remoteSyncer.Spec.TargetStrategy == syngit.OneTarget && len(wrc.remoteTargets) > 1 { + errMsg := "multiple RemoteTargets found for OneTarget set as the TargetStrategy in the RemoteSyncer" + details.messageAddition = errMsg + return false, errors.New(errMsg) + } + + if len(wrc.remoteTargets) == 0 { + errMsg := "no RemoteTarget found" + details.messageAddition = errMsg + return false, errors.New(errMsg) + } + } + + if remoteUserBinding == nil { // Check if there is a default user that we can use if wrc.remoteSyncer.Spec.DefaultUnauthorizedUserMode != syngit.UseDefaultUser || wrc.remoteSyncer.Spec.DefaultRemoteUserRef == nil || wrc.remoteSyncer.Spec.DefaultRemoteUserRef.Name == "" { @@ -222,20 +342,20 @@ func (wrc *WebhookRequestChecker) userAllowed(details *wrcDetails) (bool, error) } // Search for the default RemoteUser object - namespacedName := &types.NamespacedName{ + userNamespacedName := &types.NamespacedName{ Namespace: wrc.remoteSyncer.Namespace, Name: wrc.remoteSyncer.Spec.DefaultRemoteUserRef.Name, } remoteUser := &syngit.RemoteUser{} - err := wrc.k8sClient.Get(ctx, *namespacedName, remoteUser) + err := wrc.k8sClient.Get(ctx, *userNamespacedName, remoteUser) if err != nil { - errMsg := "the default user is not found : " + wrc.remoteSyncer.Spec.DefaultRemoteUserRef.Name + errMsg := "the default RemoteUser is not found : " + wrc.remoteSyncer.Spec.DefaultRemoteUserRef.Name details.messageAddition = errMsg return false, err } if remoteUser.Spec.GitBaseDomainFQDN != fqdn { - errMsg := "the fqdn of the default user does not match the associated RemoteSyncer (" + wrc.remoteSyncer.Name + ") fqdn (" + remoteUser.Spec.GitBaseDomainFQDN + ")" + errMsg := "the fqdn of the default RemoteUser does not match the associated RemoteSyncer (" + wrc.remoteSyncer.Name + ") fqdn (" + remoteUser.Spec.GitBaseDomainFQDN + ")" details.messageAddition = errMsg return false, err } @@ -245,12 +365,27 @@ func (wrc *WebhookRequestChecker) userAllowed(details *wrcDetails) (bool, error) details.messageAddition = errMsg return false, err } - } - if userCountLoop > 1 { - const errMsg = "multiple RemoteUserBinding found OR the name of the user is not unique; this version of the operator work with the name as unique identifier for users" - details.messageAddition = errMsg - return false, errors.New(errMsg) + // Search for the default RemoteTarget + targetNamespacedName := &types.NamespacedName{ + Namespace: wrc.remoteSyncer.Namespace, + Name: wrc.remoteSyncer.Spec.DefaultRemoteTargetRef.Name, + } + remoteTarget := &syngit.RemoteTarget{} + err = wrc.k8sClient.Get(ctx, *targetNamespacedName, remoteTarget) + if err != nil { + errMsg := "the default RemoteTarget is not found : " + wrc.remoteSyncer.Spec.DefaultRemoteTargetRef.Name + details.messageAddition = errMsg + return false, err + } + + if remoteTarget.Spec.UpstreamRepository != wrc.remoteSyncer.Spec.RemoteRepository || remoteTarget.Spec.UpstreamBranch != wrc.remoteSyncer.Spec.DefaultBranch { + errMsg := fmt.Sprintf("the RemoteSyncer's repository or branch does not match the upstream repository or branch of the default RemoteTarget. RemoteSyncer repo: %s; RemoteSyncer branch: %s; RemoteTarget upstream repo: %s; RemoteTarget upstream branch: %s", wrc.remoteSyncer.Spec.RemoteRepository, wrc.remoteSyncer.Spec.DefaultBranch, remoteTarget.Spec.UpstreamRepository, remoteTarget.Spec.UpstreamBranch) + details.messageAddition = errMsg + return false, err + } + + wrc.remoteTargets = []syngit.RemoteTarget{*remoteTarget} } details.gitUser = *gitUser @@ -472,7 +607,7 @@ func (wrc *WebhookRequestChecker) convertToYaml(details *wrcDetails) error { func (wrc *WebhookRequestChecker) tlsContructor(details *wrcDetails) error { // Step 1: Search for the global CA Bundle of the server located in the syngit namespace - caBundle, caErr := utils.FindGlobalCABundle(wrc.k8sClient, strings.Split(details.repoHost, ":")[0]) + caBundle, caErr := utils.FindGlobalCABundle(wrc.k8sClient, strings.Split(details.serverHost, ":")[0]) if caErr != nil && strings.Contains(caErr.Error(), utils.CaSecretWrongTypeErrorMessage) { details.messageAddition = caErr.Error() return caErr @@ -493,46 +628,50 @@ func (wrc *WebhookRequestChecker) tlsContructor(details *wrcDetails) error { caBundle = caBundleRsy } - details.insecureSkipTlsVerify = wrc.remoteSyncer.Spec.InsecureSkipTlsVerify details.caBundle = caBundle return nil } func (wrc *WebhookRequestChecker) gitPush(details *wrcDetails) (bool, error) { - gitPusher := &GitPusher{ - remoteSyncer: *wrc.remoteSyncer.DeepCopy(), - interceptedYAML: details.interceptedYAML, - interceptedGVR: details.interceptedGVR, - interceptedName: details.interceptedName, - gitUser: details.gitUser.gitUser, - gitEmail: details.gitUser.gitEmail, - gitToken: details.gitUser.gitToken, - operation: wrc.admReview.Request.Operation, - insecureSkipTlsVerify: details.insecureSkipTlsVerify, - caBundle: details.caBundle, - } - res, err := gitPusher.Push() - if err != nil { - errMsg := err.Error() - details.messageAddition = errMsg - return false, errors.New(errMsg) - } - if res.commitHash == "" { - return false, nil - } + for _, remoteTarget := range wrc.remoteTargets { + gitPusher := &GitPusher{ + remoteSyncer: *wrc.remoteSyncer.DeepCopy(), + remoteTarget: *remoteTarget.DeepCopy(), + interceptedYAML: details.interceptedYAML, + interceptedGVR: details.interceptedGVR, + interceptedName: details.interceptedName, + gitUser: details.gitUser.gitUser, + gitEmail: details.gitUser.gitEmail, + gitToken: details.gitUser.gitToken, + operation: wrc.admReview.Request.Operation, + caBundle: details.caBundle, + } + res, err := gitPusher.Push() + if err != nil { + errMsg := err.Error() + details.messageAddition = errMsg + return false, errors.New(errMsg) + } + + if res.commitHash == "" { + return false, nil + } - details.repoPath = res.path - details.commitHash = res.commitHash - details.repoUrl = wrc.remoteSyncer.Spec.RemoteRepository + details.targetsPushInformation = append(details.targetsPushInformation, pushInformation{ + repoPath: res.path, + commitHash: res.commitHash, + repoUrl: res.url, + }) + } return true, nil } -func (wrc *WebhookRequestChecker) gitPushPostChecker(isPushed bool, err error, details *wrcDetails) { - details.processPass = isPushed - if isPushed { +func (wrc *WebhookRequestChecker) gitPushPostChecker(areTheyPushed bool, err error, details *wrcDetails) { + details.processPass = areTheyPushed + if areTheyPushed { details.pushDetails = "Resource successfully pushed" } else { details.pushDetails = err.Error() @@ -597,18 +736,6 @@ func (wrc *WebhookRequestChecker) responseConstructor(details wrcDetails) admiss message += " " + details.messageAddition } - // Annotation that will be stored in the outcoming object - auditAnnotation := make(map[string]string) - if details.repoUrl != "" { - auditAnnotation["syngit-git-repo-fqdn"] = details.repoUrl - } - if details.repoPath != "" { - auditAnnotation["syngit-git-repo-path"] = details.repoPath - } - if details.commitHash != "" { - auditAnnotation["syngit-git-commit-hash"] = details.commitHash - } - // Construct the admisson review request admissionReviewResp := admissionv1.AdmissionReview{ Response: &admissionv1.AdmissionResponse{ @@ -618,7 +745,6 @@ func (wrc *WebhookRequestChecker) responseConstructor(details wrcDetails) admiss Status: status, Message: message, }, - AuditAnnotations: auditAnnotation, }, } admissionReviewResp.SetGroupVersionKind(schema.GroupVersionKind{ @@ -676,6 +802,21 @@ func (wrc *WebhookRequestChecker) updateStatusState(kind string, details wrcDeta Resource: details.interceptedGVR.Resource, Name: details.interceptedName, } + + repos := []string{} + for _, info := range details.targetsPushInformation { + repos = append(repos, info.repoUrl) + } + commitHashes := []string{} + for _, info := range details.targetsPushInformation { + commitHashes = append(commitHashes, info.commitHash) + } + + repoPath := "" + if len(details.targetsPushInformation) > 0 { + repoPath = details.targetsPushInformation[0].repoPath + } + switch kind { case "LastBypassedObjectState": lastBypassedObjectState := &syngit.LastBypassedObjectState{ @@ -693,13 +834,13 @@ func (wrc *WebhookRequestChecker) updateStatusState(kind string, details wrcDeta wrc.remoteSyncer.Status.LastObservedObjectState = *lastObservedObjectState case "LastPushedObjectState": lastPushedObjectState := &syngit.LastPushedObjectState{ - LastPushedObjectTime: v1.Now(), - LastPushedObject: *gvrn, - LastPushedObjectGitPath: details.repoPath, - LastPushedObjectGitRepo: details.repoUrl, - LastPushedObjectGitCommitHash: details.commitHash, - LastPushedGitUser: details.gitUser.gitUser, - LastPushedObjectStatus: details.pushDetails, + LastPushedObjectTime: v1.Now(), + LastPushedObject: *gvrn, + LastPushedObjectGitPath: repoPath, + LastPushedObjectGitRepos: repos, + LastPushedObjectGitCommitHashes: commitHashes, + LastPushedGitUser: details.gitUser.gitUser, + LastPushedObjectStatus: details.pushDetails, } wrc.remoteSyncer.Status.LastPushedObjectState = *lastPushedObjectState } diff --git a/internal/patterns/v1beta3/patterns_types.go b/internal/patterns/v1beta3/patterns_types.go new file mode 100644 index 0000000..043f869 --- /dev/null +++ b/internal/patterns/v1beta3/patterns_types.go @@ -0,0 +1,53 @@ +package v1beta3 + +import ( + "context" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type PatternSpecification struct { + Client client.Client + NamespacedName types.NamespacedName +} + +type Pattern interface { + Setup(ctx context.Context) *ErrorPattern + Diff(ctx context.Context) *ErrorPattern + Remove(ctx context.Context) *ErrorPattern +} + +type reason string + +const ( + Errored reason = "Errored" + Denied reason = "Denied" +) + +type ErrorPattern struct { + Message string + Reason reason +} + +func (e *ErrorPattern) Error() string { + return e.Message +} + +func Trigger(p Pattern, ctx context.Context) *ErrorPattern { + + diffErr := p.Diff(ctx) + if diffErr != nil { + return diffErr + } + removeErr := p.Remove(ctx) + if removeErr != nil { + return removeErr + } + setupErr := p.Setup(ctx) + if setupErr != nil { + return setupErr + } + + return nil +} diff --git a/internal/patterns/v1beta3/remotesyncer_userspecific_pattern.go b/internal/patterns/v1beta3/remotesyncer_userspecific_pattern.go new file mode 100644 index 0000000..032ebf3 --- /dev/null +++ b/internal/patterns/v1beta3/remotesyncer_userspecific_pattern.go @@ -0,0 +1,238 @@ +package v1beta3 + +import ( + "context" + "fmt" + + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + "github.com/syngit-org/syngit/pkg/utils" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type UserSpecificPattern struct { + PatternSpecification + Username string + RemoteSyncer syngit.RemoteSyncer + associatedRemoteTarget syngit.RemoteTarget + remoteTargetToBeSetuped *syngit.RemoteTarget + remoteTargetsToBeRemoved []syngit.RemoteTarget + RemoteUserBinding *syngit.RemoteUserBinding +} + +func (usp *UserSpecificPattern) GetRemoteTarget() syngit.RemoteTarget { + return usp.associatedRemoteTarget +} + +func (usp *UserSpecificPattern) Setup(ctx context.Context) *ErrorPattern { + if usp.remoteTargetToBeSetuped != nil { + createErr := usp.Client.Create(ctx, usp.remoteTargetToBeSetuped) + if createErr != nil { + return &ErrorPattern{Message: createErr.Error(), Reason: Errored} + } + + spec := usp.RemoteUserBinding.Spec.DeepCopy() + spec.RemoteTargetRefs = append(spec.RemoteTargetRefs, corev1.ObjectReference{ + Name: usp.remoteTargetToBeSetuped.Name, + }) + updateErr := updateOrDeleteRemoteUserBinding(ctx, usp.Client, *spec, *usp.RemoteUserBinding, 2) + if updateErr != nil { + return &ErrorPattern{Message: updateErr.Error(), Reason: Errored} + } + } + + return nil +} + +func (usp *UserSpecificPattern) Remove(ctx context.Context) *ErrorPattern { + if len(usp.remoteTargetsToBeRemoved) > 0 { + remoteTargetsRef := []corev1.ObjectReference{} + + for _, rt := range usp.remoteTargetsToBeRemoved { + // Delete RemoteTarget + remoteTarget := &syngit.RemoteTarget{} + namespacedName := types.NamespacedName{Name: rt.Name, Namespace: rt.Namespace} + getErr := usp.Client.Get(ctx, namespacedName, remoteTarget) + if getErr != nil { + return &ErrorPattern{Message: getErr.Error(), Reason: Errored} + } + delErr := usp.Client.Delete(ctx, remoteTarget) + if delErr != nil { + return &ErrorPattern{Message: delErr.Error(), Reason: Errored} + } + + // Unreference it from the associated RemoteUserBinding + if usp.RemoteUserBinding == nil { + return &ErrorPattern{Message: "Server error: no associated RemoteUserBinding found", Reason: Errored} + } + for _, remoteTargetRef := range usp.RemoteUserBinding.Spec.RemoteTargetRefs { + if remoteTargetRef.Name != rt.Name { + remoteTargetsRef = append(remoteTargetsRef, remoteTargetRef) + } + } + } + + // Apply the unreferencement + spec := usp.RemoteUserBinding.Spec.DeepCopy() + spec.RemoteTargetRefs = remoteTargetsRef + updateErr := updateOrDeleteRemoteUserBinding(ctx, usp.Client, *spec, *usp.RemoteUserBinding, 2) + if updateErr != nil { + return &ErrorPattern{Message: updateErr.Error(), Reason: Errored} + } + } + + return nil +} + +func (usp *UserSpecificPattern) Diff(ctx context.Context) *ErrorPattern { + usp.remoteTargetsToBeRemoved = []syngit.RemoteTarget{} + + // Get associated RemoteUserBinding + remoteUserBindingList := &syngit.RemoteUserBindingList{} + listOps := &client.ListOptions{ + LabelSelector: labels.SelectorFromSet(labels.Set{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.K8sUserLabelKey: usp.Username, + }), + Namespace: usp.RemoteSyncer.Namespace, + } + listErr := usp.Client.List(ctx, remoteUserBindingList, listOps) + if listErr != nil { + return &ErrorPattern{Message: listErr.Error(), Reason: Errored} + } + if len(remoteUserBindingList.Items) > 1 { + return &ErrorPattern{Message: fmt.Sprintf("only one RemoteUserBinding for the user %s should be managed by Syngit", usp.Username), Reason: Denied} + } + if len(remoteUserBindingList.Items) == 0 { + return nil + } + + if usp.RemoteUserBinding == nil { + remoteUserBinding := &syngit.RemoteUserBinding{} + namespacedName := types.NamespacedName{Name: remoteUserBindingList.Items[0].Name, Namespace: remoteUserBindingList.Items[0].Namespace} + getErr := usp.Client.Get(ctx, namespacedName, remoteUserBinding) + if getErr != nil { + return &ErrorPattern{Message: getErr.Error(), Reason: Errored} + } + usp.RemoteUserBinding = remoteUserBinding + } + + // Get RemoteTargets that are already bound to this user + // Scope only the RemoteTargets with the same upstream repo & branch as the RemoteSyncer + boundRemoteTargets, listErr := usp.getExistingRemoteTarget(ctx) + if listErr != nil { + return &ErrorPattern{Message: listErr.Error(), Reason: Errored} + } + + // If some are bound and there is no user specific annotation anymore + userSpecificAnnotation := usp.RemoteSyncer.Annotations[syngit.RtAnnotationUserSpecificKey] + if userSpecificAnnotation == "" { + if len(boundRemoteTargets) > 0 { + usp.remoteTargetsToBeRemoved = boundRemoteTargets + } + return nil + } + + alreadyExists := false + for _, rt := range boundRemoteTargets { + if userSpecificAnnotation == string(syngit.RtAnnotationOneUserOneBranchValue) { + // If the upstream repo & branch are the same (already filtered), the target repo is the same as the upstream and the branch is the username. + // An user specific target could be different branch on the same repo (target-branch != upstream-branch) + if rt.Spec.UpstreamBranch == usp.RemoteSyncer.Spec.DefaultBranch && rt.Spec.TargetRepository == usp.RemoteSyncer.Spec.RemoteRepository && rt.Spec.TargetBranch == usp.Username { + usp.associatedRemoteTarget = rt + alreadyExists = true + break + } + } + + if userSpecificAnnotation == string(syngit.RtAnnotationOneUserOneForkValue) { + // If the upstream repo & branch are the same (already filtered), then it is considered as found. + // To allow permissive extension for external providers, we consider that the scope is the most open as possible. + // An user specific target could be a fork (target-repo != upstream-repo) + if rt.Spec.UpstreamBranch == usp.RemoteSyncer.Spec.DefaultBranch { + usp.associatedRemoteTarget = rt + alreadyExists = true + break + } + } + } + + if alreadyExists { + return nil + } + + // If the remoteTarget does not exists yet AND has to be created + targetRepo := usp.RemoteSyncer.Spec.RemoteRepository + if userSpecificAnnotation == string(syngit.RtAnnotationOneUserOneForkValue) { + // Set it to empty because we do not know in advance the name of the fork + // It will later be fill by the provider + targetRepo = "" + } + builtRemoteTarget, buildErr := usp.buildRemoteTarget(targetRepo) + if buildErr != nil { + return &ErrorPattern{Message: buildErr.Error(), Reason: Errored} + } + + usp.remoteTargetToBeSetuped = builtRemoteTarget + usp.associatedRemoteTarget = *usp.remoteTargetToBeSetuped + + return nil +} + +func (usp *UserSpecificPattern) getExistingRemoteTarget(ctx context.Context) ([]syngit.RemoteTarget, error) { + var remoteTargetList = &syngit.RemoteTargetList{} + listOps := &client.ListOptions{ + LabelSelector: labels.SelectorFromSet(labels.Set{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.K8sUserLabelKey: usp.Username, + }), + Namespace: usp.RemoteSyncer.Namespace, + } + err := usp.Client.List(ctx, remoteTargetList, listOps) + if err != nil { + return nil, err + } + + remoteTargets := []syngit.RemoteTarget{} + for _, rt := range remoteTargetList.Items { + if rt.Spec.UpstreamRepository == usp.RemoteSyncer.Spec.RemoteRepository && rt.Spec.UpstreamBranch == usp.RemoteSyncer.Spec.DefaultBranch { + remoteTargets = append(remoteTargets, rt) + } + } + + return remoteTargets, nil +} + +func (usp *UserSpecificPattern) buildRemoteTarget(targetRepo string) (*syngit.RemoteTarget, error) { + + rtName, nameErr := utils.RemoteTargetNameConstructor(usp.RemoteSyncer.Spec.RemoteRepository, usp.RemoteSyncer.Spec.DefaultBranch, targetRepo, usp.Username) + if nameErr != nil { + return nil, nameErr + } + + remoteTarget := &syngit.RemoteTarget{ + ObjectMeta: v1.ObjectMeta{ + Name: rtName, + Namespace: usp.RemoteSyncer.Namespace, + Labels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.K8sUserLabelKey: usp.Username, + }, + Annotations: map[string]string{ + syngit.RtAllowInjection: "true", + }, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: usp.RemoteSyncer.Spec.RemoteRepository, + UpstreamBranch: usp.RemoteSyncer.Spec.DefaultBranch, + TargetRepository: targetRepo, + TargetBranch: usp.Username, + MergeStrategy: syngit.TryFastForwardOrHardReset, + }, + } + + return remoteTarget, nil +} diff --git a/internal/patterns/v1beta3/remotesyncers_oneormanybranches.go b/internal/patterns/v1beta3/remotesyncers_oneormanybranches.go new file mode 100644 index 0000000..2de4d16 --- /dev/null +++ b/internal/patterns/v1beta3/remotesyncers_oneormanybranches.go @@ -0,0 +1,275 @@ +package v1beta3 + +import ( + "context" + "slices" + + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + "github.com/syngit-org/syngit/pkg/utils" + v1 "k8s.io/api/core/v1" + 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" +) + +type RemoteSyncerOneOrManyBranchPattern struct { + PatternSpecification + UpstreamRepo string + UpstreamBranch string + TargetRepository string + OldTargetBranches []string + NewTargetBranches []string + remoteTargetsToBeRemoved []syngit.RemoteTarget + remoteTargetsToBeSetup []*syngit.RemoteTarget + remoteUserBindings *syngit.RemoteUserBindingList +} + +func (rsomp *RemoteSyncerOneOrManyBranchPattern) Setup(ctx context.Context) *ErrorPattern { + for _, remoteTarget := range rsomp.remoteTargetsToBeSetup { + // Create the RemoteTargets + createOrUpdateErr := createOrUpdateRemoteTarget(ctx, rsomp.Client, remoteTarget) + if createOrUpdateErr != nil { + return &ErrorPattern{Message: createOrUpdateErr.Error(), Reason: Errored} + } + } + + // Associate to all the RemoteUserBindings + associationErr := rsomp.addRemoteUserBindingAssociation(ctx, rsomp.remoteTargetsToBeSetup, *rsomp.remoteUserBindings) + if associationErr != nil { + return &ErrorPattern{Message: associationErr.Error(), Reason: Errored} + } + + return nil +} + +func (rsomp *RemoteSyncerOneOrManyBranchPattern) addRemoteUserBindingAssociation(ctx context.Context, remoteTargets []*syngit.RemoteTarget, remoteUserBindings syngit.RemoteUserBindingList) error { + for _, rub := range remoteUserBindings.Items { + spec := rub.Spec + newRemoteTargetRefs := spec.RemoteTargetRefs + for _, rt := range remoteTargets { + newRemoteTargetRefs = append(newRemoteTargetRefs, v1.ObjectReference{Name: rt.Name}) + } + + spec.RemoteTargetRefs = newRemoteTargetRefs + err := updateOrDeleteRemoteUserBinding(ctx, rsomp.Client, spec, rub, 2) + if err != nil { + return err + } + } + return nil +} + +func (rsomp *RemoteSyncerOneOrManyBranchPattern) Remove(ctx context.Context) *ErrorPattern { + + for _, rt := range rsomp.remoteTargetsToBeRemoved { + // Delete RemoteTarget + delErr := rsomp.Client.Delete(ctx, &rt) + if delErr != nil { + return &ErrorPattern{Message: delErr.Error(), Reason: Errored} + } + } + + // Remove association from RemoteUserBindings + associationErr := rsomp.removeRemoteUserBindingAssociation(ctx, rsomp.remoteTargetsToBeRemoved, *rsomp.remoteUserBindings) + if associationErr != nil { + return &ErrorPattern{Message: associationErr.Error(), Reason: Errored} + } + + return nil +} + +func (rsomp *RemoteSyncerOneOrManyBranchPattern) removeRemoteUserBindingAssociation(ctx context.Context, remoteTargets []syngit.RemoteTarget, remoteUserBindings syngit.RemoteUserBindingList) error { + for _, rub := range remoteUserBindings.Items { + spec := rub.Spec + newRemoteTargetRefs := spec.RemoteTargetRefs + + for _, rt := range remoteTargets { + for _, associatedRemoteTargetRef := range rub.Spec.RemoteTargetRefs { + if associatedRemoteTargetRef.Name != rt.Name || associatedRemoteTargetRef.Namespace != rt.Namespace { + newRemoteTargetRefs = append(newRemoteTargetRefs, associatedRemoteTargetRef) + } + } + } + + spec.RemoteTargetRefs = newRemoteTargetRefs + err := updateOrDeleteRemoteUserBinding(ctx, rsomp.Client, spec, rub, 2) + if err != nil { + return err + } + } + return nil +} + +func (rsomp *RemoteSyncerOneOrManyBranchPattern) Diff(ctx context.Context) *ErrorPattern { + + listOps := &client.ListOptions{ + Namespace: rsomp.NamespacedName.Namespace, + LabelSelector: labels.SelectorFromSet(labels.Set{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + }), + } + + // Get all RemoteUserBinding of the namespace + rsomp.remoteUserBindings = &syngit.RemoteUserBindingList{} + listErr := rsomp.Client.List(ctx, rsomp.remoteUserBindings, listOps) + if listErr != nil { + return &ErrorPattern{Message: listErr.Error(), Reason: Errored} + } + + // Get all RemoteTargets of the namespace + allRemoteTargets := &syngit.RemoteTargetList{} + listErr = rsomp.Client.List(ctx, allRemoteTargets, listOps) + if listErr != nil { + return &ErrorPattern{Message: listErr.Error(), Reason: Errored} + } + // Get only the RemoteTargets that target the same: + // - upstream repo + // - upstream branch + // - target repo + // - target branches + // Then, filter the difference between the old and the new branches. + // Filter out the dependencies with other RemoteSyncers + // Finally, re-add the new branches + var filterErr error + rsomp.remoteTargetsToBeRemoved, filterErr = rsomp.getRemoteTargetsToBeRemoved(ctx, *allRemoteTargets) + if filterErr != nil { + return &ErrorPattern{Message: filterErr.Error(), Reason: Errored} + } + + rsomp.remoteTargetsToBeSetup, filterErr = rsomp.getRemoteTargetsToBeSetup(*allRemoteTargets) + if filterErr != nil { + return &ErrorPattern{Message: filterErr.Error(), Reason: Errored} + } + + return nil +} + +func (rsomp *RemoteSyncerOneOrManyBranchPattern) getRemoteTargetsToBeSetup(in syngit.RemoteTargetList) ([]*syngit.RemoteTarget, error) { + branches := []string{} + + // Search for the branches that are not already implemented in a RemoteTarget managed by Syngit + for _, branch := range rsomp.NewTargetBranches { + found := false + for _, rt := range in.Items { + spec := rt.Spec + if spec.UpstreamRepository == rsomp.UpstreamRepo && spec.UpstreamBranch == rsomp.UpstreamBranch && spec.TargetRepository == rsomp.TargetRepository { + if spec.TargetBranch == branch { + found = true + break + } + } + } + if !found { + branches = append(branches, branch) + } + } + + out := []*syngit.RemoteTarget{} + for _, branch := range branches { + name, nameErr := utils.RemoteTargetNameConstructor(rsomp.UpstreamRepo, rsomp.UpstreamBranch, rsomp.UpstreamRepo, branch) + if nameErr != nil { + return nil, nameErr + } + mergeStrategy := syngit.TryFastForwardOrHardReset + if rsomp.UpstreamBranch == branch { + mergeStrategy = "" + } + remoteTarget := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: rsomp.NamespacedName.Namespace, + Labels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: branch, + syngit.RtLabelPatternKey: syngit.RtLabelOneOrManyBranchesValue, + }, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: rsomp.UpstreamRepo, + UpstreamBranch: rsomp.UpstreamBranch, + TargetRepository: rsomp.TargetRepository, + TargetBranch: branch, + MergeStrategy: mergeStrategy, + }, + } + out = append(out, remoteTarget) + } + + return out, nil +} + +func (rsomp *RemoteSyncerOneOrManyBranchPattern) getRemoteTargetsToBeRemoved(ctx context.Context, in syngit.RemoteTargetList) ([]syngit.RemoteTarget, error) { + out := []syngit.RemoteTarget{} + + // Only filter for the branches that are actually not used anymore by the current RemoteTarget + diff := slicesDifference(rsomp.OldTargetBranches, rsomp.NewTargetBranches) + oldBranches := []string{} + for _, branch := range diff { + if !slices.Contains(rsomp.NewTargetBranches, branch) { + oldBranches = append(oldBranches, branch) + } + } + + // Search for non-dependent branches + deletable, depErr := rsomp.getBranchesToBeRemoved(ctx, oldBranches) + if depErr != nil { + return nil, depErr + } + + for _, rt := range in.Items { + spec := rt.Spec + if spec.UpstreamRepository == rsomp.UpstreamRepo && spec.UpstreamBranch == rsomp.UpstreamBranch && spec.TargetRepository == rsomp.TargetRepository { + for _, branch := range deletable { + if spec.TargetBranch == branch { + out = append(out, rt) + } + } + } + } + return out, nil +} + +// Search for automatically created RemoteTargets that are NOT used by any other RemoteSyncer +func (rsomp *RemoteSyncerOneOrManyBranchPattern) getBranchesToBeRemoved(ctx context.Context, branches []string) ([]string, error) { + out := map[string]bool{} + for _, branch := range branches { + out[branch] = true + } + + remoteSyncers := &syngit.RemoteSyncerList{} + selector := labels.NewSelector() + requirement, reqErr := labels.NewRequirement(syngit.RtAnnotationOneOrManyBranchesKey, selection.Exists, nil) + if reqErr != nil { + return nil, reqErr + } + selector.Add(*requirement) + listOps := &client.ListOptions{ + Namespace: rsomp.NamespacedName.Namespace, + LabelSelector: selector, + } + listErr := rsomp.Client.List(ctx, remoteSyncers, listOps) + if listErr != nil { + return nil, listErr + } + + for _, remoteSyncer := range remoteSyncers.Items { + if remoteSyncer.Name != rsomp.NamespacedName.Name || remoteSyncer.Namespace != rsomp.NamespacedName.Namespace { + remoteSyncerBranches := utils.GetBranchesFromAnnotation(remoteSyncer.Annotations[syngit.RtAnnotationOneOrManyBranchesKey]) + for _, branch := range branches { + if slices.Contains(remoteSyncerBranches, branch) { + out[branch] = false + } + } + } + } + + branchesToBeRemoved := []string{} + for branch, toBeRemoved := range out { + if toBeRemoved { + branchesToBeRemoved = append(branchesToBeRemoved, branch) + } + } + + return branchesToBeRemoved, nil +} diff --git a/internal/patterns/v1beta3/remoteuser_association.go b/internal/patterns/v1beta3/remoteuser_association.go new file mode 100644 index 0000000..449787e --- /dev/null +++ b/internal/patterns/v1beta3/remoteuser_association.go @@ -0,0 +1,221 @@ +package v1beta3 + +import ( + "context" + "fmt" + "strings" + + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type RemoteUserAssociationPattern struct { + PatternSpecification + RemoteUser syngit.RemoteUser + IsEnabled bool + Username string + associatedRemoteUserBinding *syngit.RemoteUserBinding + hasToBeRemoved bool + hasToBeSetup bool +} + +func (ruap *RemoteUserAssociationPattern) Remove(ctx context.Context) *ErrorPattern { + if ruap.hasToBeRemoved { + // Select the first one because there must not be more than one + rub := ruap.associatedRemoteUserBinding + // Remove the RemoteUser from the associated RemoteUserBinding + if err := ruap.removeRuFromRub(ctx, rub); err != nil { + return &ErrorPattern{Message: err.Error(), Reason: Errored} + } + } + return nil +} + +func (ruap *RemoteUserAssociationPattern) Setup(ctx context.Context) *ErrorPattern { + if ruap.hasToBeSetup { + updateErr := updateOrDeleteRemoteUserBinding(ctx, ruap.Client, ruap.associatedRemoteUserBinding.Spec, *ruap.associatedRemoteUserBinding, 2) + if updateErr != nil { + if !strings.Contains(updateErr.Error(), "not found") { + return &ErrorPattern{Message: updateErr.Error(), Reason: Errored} + } + createErr := ruap.Client.Create(ctx, ruap.associatedRemoteUserBinding) + if createErr != nil { + return &ErrorPattern{Message: createErr.Error(), Reason: Errored} + } + } + } + + return nil +} + +func (ruap *RemoteUserAssociationPattern) Diff(ctx context.Context) *ErrorPattern { + + ruap.hasToBeRemoved = false + ruap.hasToBeSetup = false + + // Check if the association is already done + isAlreadyDefined, existingRemoteUserBindingName, diffErr := ruap.isAlreadyReferenced(ctx, ruap.RemoteUser.Name, ruap.NamespacedName.Namespace) + if diffErr != nil { + return &ErrorPattern{Message: diffErr.Error(), Reason: Errored} + } + + name := syngit.RubPrefix + ruap.Username + + // List all the RemoteUserBindings that are associated to this user and managed by Syngit. + remoteUserBindingList := &syngit.RemoteUserBindingList{} + listOps := &client.ListOptions{ + LabelSelector: labels.SelectorFromSet(labels.Set{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.K8sUserLabelKey: ruap.Username, + }), + Namespace: ruap.NamespacedName.Namespace, + } + rubErr := ruap.Client.List(ctx, remoteUserBindingList, listOps) + if rubErr != nil { + return &ErrorPattern{Message: rubErr.Error(), Reason: Errored} + } + + if len(remoteUserBindingList.Items) > 1 { + return &ErrorPattern{Message: fmt.Sprintf("only one RemoteUserBinding for the user %s should be managed by Syngit", ruap.Username), Reason: Denied} + } + + objRef := corev1.ObjectReference{Name: ruap.RemoteUser.Name} + + if len(remoteUserBindingList.Items) <= 0 { + + if !ruap.IsEnabled { + // The pattern is not enabled and no RemoteUserBinding is associated + return nil + } + + if isAlreadyDefined && name != existingRemoteUserBindingName { + return &ErrorPattern{Message: fmt.Sprintf("the RemoteUser is already bound in the RemoteUserBinding %s", existingRemoteUserBindingName), Reason: Denied} + } + + // CREATE the RemoteUserBinding + + rub := &syngit.RemoteUserBinding{} + rub.SetName(name) + rub.SetNamespace(ruap.RemoteUser.Namespace) + + // Geneate the RUB object with the right name + rubName, generateErr := generateName(ctx, ruap.Client, rub.DeepCopy(), 0) + if generateErr != nil { + return &ErrorPattern{Message: generateErr.Error(), Reason: Errored} + } + rub.SetName(rubName) + + // Set the labels + rub.Labels = map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.K8sUserLabelKey: ruap.Username, + } + + subject := &rbacv1.Subject{ + Kind: "User", + Name: ruap.Username, + } + rub.Spec.Subject = *subject + + remoteRefs := make([]corev1.ObjectReference, 0) + remoteRefs = append(remoteRefs, objRef) + rub.Spec.RemoteUserRefs = remoteRefs + + ruap.associatedRemoteUserBinding = rub + if ruap.IsEnabled { + ruap.hasToBeSetup = true + } + + } else { + // UPDATE or DELETE + // The RemoteUserBinding already exists + + rub := remoteUserBindingList.Items[0] + name = rub.Name + + if isAlreadyDefined && name != existingRemoteUserBindingName { + return &ErrorPattern{Message: fmt.Sprintf("the RemoteUser is already bound in the RemoteUserBinding %s", existingRemoteUserBindingName), Reason: Denied} + } + + remoteUserBinding := &syngit.RemoteUserBinding{} + + if getErr := ruap.Client.Get(ctx, types.NamespacedName{Name: rub.Name, Namespace: rub.Namespace}, remoteUserBinding); getErr != nil { + return &ErrorPattern{Message: getErr.Error(), Reason: Errored} + } + + ruap.associatedRemoteUserBinding = remoteUserBinding + + if !ruap.IsEnabled { + // The pattern is not enabled and a RemoteUserBinding is associated + // So it is a delete operation + ruap.hasToBeRemoved = true + } + + // Is the current RemoteUser already associated? + for _, remoteUserRef := range remoteUserBinding.Spec.RemoteUserRefs { + if remoteUserRef.Name == ruap.RemoteUser.Name { + return nil + } + } + + remoteUserBinding.Spec.RemoteUserRefs = append(remoteUserBinding.Spec.RemoteUserRefs, objRef) + + if ruap.IsEnabled { + ruap.hasToBeSetup = true + } + ruap.associatedRemoteUserBinding = remoteUserBinding + + } + + return nil +} + +// Search for a RemoteUserBinding that reference this k8s user. +// It can already be referenced by ANOTHER user. +func (ruap *RemoteUserAssociationPattern) isAlreadyReferenced(ctx context.Context, ruName string, ruNamespace string) (bool, string, error) { + rubs := &syngit.RemoteUserBindingList{} + listOps := &client.ListOptions{ + LabelSelector: labels.SelectorFromSet(labels.Set{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + }), + Namespace: ruNamespace, + } + rubErr := ruap.Client.List(ctx, rubs, listOps) + if rubErr != nil { + return false, "", rubErr + } + + for _, rub := range rubs.Items { + for _, ru := range rub.Spec.RemoteUserRefs { + if ru.Name == ruName { + if ru.Namespace == "" || (ru.Namespace == ruNamespace) { + return true, rub.Name, nil + } + } + } + } + + return false, "", nil +} + +func (ruap *RemoteUserAssociationPattern) removeRuFromRub(ctx context.Context, rub *syngit.RemoteUserBinding) error { + remoteRefs := rub.Spec.DeepCopy().RemoteUserRefs + newRemoteRefs := []corev1.ObjectReference{} + for _, rm := range remoteRefs { + if rm.Name != ruap.RemoteUser.Name { + newRemoteRefs = append(newRemoteRefs, rm) + } + } + rub.Spec.RemoteUserRefs = newRemoteRefs + + updateErr := updateOrDeleteRemoteUserBinding(ctx, ruap.Client, rub.Spec, *rub, 0) + if updateErr != nil { + return updateErr + } + + return nil +} diff --git a/internal/patterns/v1beta3/remoteuser_searchremotetarget.go b/internal/patterns/v1beta3/remoteuser_searchremotetarget.go new file mode 100644 index 0000000..a4f816e --- /dev/null +++ b/internal/patterns/v1beta3/remoteuser_searchremotetarget.go @@ -0,0 +1,119 @@ +package v1beta3 + +import ( + "context" + "fmt" + + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type RemoteUserSearchRemoteTargetPattern struct { + PatternSpecification + RemoteUser syngit.RemoteUser + Username string + IsEnabled bool + RemoteUserBinding *syngit.RemoteUserBinding + remoteTargetsToBeAdded []v1.ObjectReference +} + +func (rusp *RemoteUserSearchRemoteTargetPattern) Setup(ctx context.Context) *ErrorPattern { + if len(rusp.remoteTargetsToBeAdded) > 0 { + rusp.RemoteUserBinding.Spec.RemoteTargetRefs = append(rusp.RemoteUserBinding.Spec.RemoteTargetRefs, rusp.remoteTargetsToBeAdded...) + updateErr := updateOrDeleteRemoteUserBinding(ctx, rusp.Client, rusp.RemoteUserBinding.Spec, *rusp.RemoteUserBinding, 2) + if updateErr != nil { + return &ErrorPattern{Message: updateErr.Error(), Reason: Errored} + } + } + return nil +} + +func (rusp *RemoteUserSearchRemoteTargetPattern) Remove(ctx context.Context) *ErrorPattern { + // Nothing to remove since it will automatically be done by the RemoteSyncer patterns + return nil +} + +func (rusp *RemoteUserSearchRemoteTargetPattern) Diff(ctx context.Context) *ErrorPattern { + rusp.remoteTargetsToBeAdded = []v1.ObjectReference{} + + if !rusp.IsEnabled { + return nil + } + + // Get the associated RemoteUserBinding + rubListOps := &client.ListOptions{ + Namespace: rusp.NamespacedName.Namespace, + LabelSelector: labels.SelectorFromSet(labels.Set{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.K8sUserLabelKey: rusp.Username, + }), + } + remoteUserBindingList := &syngit.RemoteUserBindingList{} + listErr := rusp.Client.List(ctx, remoteUserBindingList, rubListOps) + if listErr != nil { + return &ErrorPattern{Message: listErr.Error(), Reason: Errored} + } + + if len(remoteUserBindingList.Items) > 1 { + return &ErrorPattern{Message: fmt.Sprintf("only one RemoteUserBinding for the user %s should be managed by Syngit", rusp.Username), Reason: Denied} + } + if len(remoteUserBindingList.Items) == 0 { + return &ErrorPattern{Message: fmt.Sprintf("webhook server error: no RemoteUserBinding found for %s", rusp.Username), Reason: Errored} + } + rusp.RemoteUserBinding = &remoteUserBindingList.Items[0] + + existingRemoteTargets, getErr := rusp.getRemoteTargetsThatShouldBeAssociated(ctx) + if getErr != nil { + return &ErrorPattern{Message: getErr.Error(), Reason: Errored} + } + + // Get all the RemoteTargets of bound to the current RemoteUserBinding + alreadyBoundRemoteTargetsRef := rusp.RemoteUserBinding.Spec.RemoteTargetRefs + + // Fill the slice by the remotetargets that are not yet referenced + for _, rt := range existingRemoteTargets { + found := false + for _, rtRef := range alreadyBoundRemoteTargetsRef { + if rt.Name == rtRef.Name { + found = true + break + } + } + if !found { + remoteTargetRef := v1.ObjectReference{ + Name: rt.Name, + } + rusp.remoteTargetsToBeAdded = append(rusp.remoteTargetsToBeAdded, remoteTargetRef) + } + } + + return nil +} + +func (rusp *RemoteUserSearchRemoteTargetPattern) getRemoteTargetsThatShouldBeAssociated(ctx context.Context) ([]syngit.RemoteTarget, error) { + remoteTargets := []syngit.RemoteTarget{} + + // Get all the RemoteTargets that should be bound to all the RemoteUserBindings of the namespace. + // In other words, all the RemoteTargets that targets a non-unique branch created by + // the usage of a syngit pattern. + rtListOps := &client.ListOptions{ + Namespace: rusp.NamespacedName.Namespace, + LabelSelector: labels.SelectorFromSet(labels.Set{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelPatternKey: syngit.RtLabelOneOrManyBranchesValue, + }), + } + ombRemoteTargetList := &syngit.RemoteTargetList{} + listErr := rusp.Client.List(ctx, ombRemoteTargetList, rtListOps) + if listErr != nil { + return nil, listErr + } + remoteTargets = append(remoteTargets, ombRemoteTargetList.Items...) + + // Create the RemoteTargets that should be specific to this user + // TODO + + return remoteTargets, nil +} diff --git a/internal/patterns/v1beta3/utils.go b/internal/patterns/v1beta3/utils.go new file mode 100644 index 0000000..695a93f --- /dev/null +++ b/internal/patterns/v1beta3/utils.go @@ -0,0 +1,132 @@ +package v1beta3 + +import ( + "context" + "fmt" + "strings" + + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func generateName(ctx context.Context, client client.Client, object client.Object, suffixNumber int) (string, error) { + oldName := object.GetName() + newName := object.GetName() + if suffixNumber > 0 { + newName = fmt.Sprintf("%s-%d", object.GetName(), suffixNumber) + } + webhookNamespacedName := &types.NamespacedName{ + Name: newName, + Namespace: object.GetNamespace(), + } + getErr := client.Get(ctx, *webhookNamespacedName, object) + if getErr == nil { + object.SetName(oldName) + return generateName(ctx, client, object, suffixNumber+1) + } else { + if strings.Contains(getErr.Error(), "not found") { + return newName, nil + } + return "", getErr + } +} + +// Update the associated RemoteUserBinding with the new spec. +// Delete the associated RemoteUserBinding if the spec is empty. +// The input must be a RemoteUserBinding managed by syngit. +// The retryNumber is used when a conflict happens. +func updateOrDeleteRemoteUserBinding(ctx context.Context, client client.Client, spec syngit.RemoteUserBindingSpec, remoteUserBinding syngit.RemoteUserBinding, retryNumber int) error { + var rub syngit.RemoteUserBinding + if err := client.Get(ctx, types.NamespacedName{Name: remoteUserBinding.Name, Namespace: remoteUserBinding.Namespace}, &rub); err != nil { + return err + } + + if len(spec.RemoteUserRefs) == 0 { + remoteUserBinding.Labels[syngit.ManagedByLabelKey] = "" + remoteUserBinding.Spec.RemoteTargetRefs = []v1.ObjectReference{} + + if len(spec.RemoteTargetRefs) == 0 { + delErr := client.Delete(ctx, &rub) + if delErr != nil { + return delErr + } + } + } + + rub.Spec = spec + if err := client.Update(ctx, &rub); err != nil { + if retryNumber > 0 { + return updateOrDeleteRemoteUserBinding(ctx, client, spec, remoteUserBinding, retryNumber-1) + } + return err + } + return nil +} + +func createOrUpdateRemoteTarget(ctx context.Context, k8sClient client.Client, remoteTarget *syngit.RemoteTarget) error { + if createErr := k8sClient.Create(ctx, remoteTarget); createErr != nil { + // If it already exists, then we skip this part + if !strings.Contains(createErr.Error(), "already exists") { + return createErr + } + } + + // Add the association to each RemoteUserBindings + rubs := &syngit.RemoteUserBindingList{} + listOps := &client.ListOptions{ + Namespace: remoteTarget.Namespace, + LabelSelector: labels.SelectorFromSet(labels.Set{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + }), + } + listErr := k8sClient.List(ctx, rubs, listOps) + if listErr != nil { + return listErr + } + + for _, rub := range rubs.Items { + newRtRefs := append(rub.Spec.DeepCopy().RemoteTargetRefs, v1.ObjectReference{ + Name: remoteTarget.Name, + }) + + spec := rub.Spec + spec.RemoteTargetRefs = newRtRefs + updateErr := updateOrDeleteRemoteUserBinding(ctx, k8sClient, spec, rub, 2) + if updateErr != nil { + return updateErr + } + } + + return nil +} + +func slicesDifference(slice1 []string, slice2 []string) []string { + var diff []string + + // Loop two times, first to find slice1 strings not in slice2, + // second loop to find slice2 strings not in slice1 + for i := 0; i < 2; i++ { + for _, s1 := range slice1 { + found := false + for _, s2 := range slice2 { + if s1 == s2 { + found = true + break + } + } + // String not found. We add it to return slice + if !found { + diff = append(diff, s1) + } + } + // Swap the slices, only if it was the first loop + if i == 0 { + slice1, slice2 = slice2, slice1 + } + } + + return diff +} diff --git a/internal/webhook/v1beta3/remotesyncer_pattern_target_webhook.go b/internal/webhook/v1beta3/remotesyncer_pattern_target_webhook.go new file mode 100644 index 0000000..7e76ef6 --- /dev/null +++ b/internal/webhook/v1beta3/remotesyncer_pattern_target_webhook.go @@ -0,0 +1,70 @@ +package v1beta3 + +import ( + "context" + "net/http" + + patterns "github.com/syngit-org/syngit/internal/patterns/v1beta3" + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + utils "github.com/syngit-org/syngit/pkg/utils" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +type RemoteSyncerTargetPatternWebhookHandler struct { + Client client.Client + Decoder *admission.Decoder +} + +// +kubebuilder:webhook:path=/syngit-v1beta3-remotesyncer-target-pattern,mutating=false,failurePolicy=fail,sideEffects=None,groups=syngit.io,resources=remotesyncers,verbs=create;update;delete,versions=v1beta3,admissionReviewVersions=v1,name=vremotesyncers-target-pattern.v1beta3.syngit.io + +func (rsyt *RemoteSyncerTargetPatternWebhookHandler) Handle(ctx context.Context, req admission.Request) admission.Response { + + var remoteSyncer *syngit.RemoteSyncer + var oldBranches = []string{} + var newBranches = []string{} + + if string(req.Operation) != "CREATE" { //nolint:goconst + remoteSyncer = &syngit.RemoteSyncer{} + err := rsyt.Decoder.DecodeRaw(req.OldObject, remoteSyncer) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + oldBranches = utils.GetBranchesFromAnnotation(remoteSyncer.Annotations[syngit.RtAnnotationOneOrManyBranchesKey]) + } + + if string(req.Operation) != "DELETE" { //nolint:goconst + remoteSyncer = &syngit.RemoteSyncer{} + err := rsyt.Decoder.Decode(req, remoteSyncer) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + newBranches = utils.GetBranchesFromAnnotation(remoteSyncer.Annotations[syngit.RtAnnotationOneOrManyBranchesKey]) + } + + pattern := &patterns.RemoteSyncerOneOrManyBranchPattern{ + PatternSpecification: patterns.PatternSpecification{ + Client: rsyt.Client, + NamespacedName: types.NamespacedName{Name: req.Name, Namespace: req.Namespace}, + }, + UpstreamRepo: remoteSyncer.Spec.RemoteRepository, + UpstreamBranch: remoteSyncer.Spec.DefaultBranch, + TargetRepository: remoteSyncer.Spec.RemoteRepository, + NewTargetBranches: newBranches, + OldTargetBranches: oldBranches, + } + + err := patterns.Trigger(pattern, ctx) + if err != nil { + if err.Reason == patterns.Denied { + return admission.Denied(err.Message) + } + if err.Reason == patterns.Errored { + return admission.Errored(http.StatusInternalServerError, err) + } + } + + return admission.Allowed("No differences concerning RemoteTargets") +} diff --git a/internal/webhook/v1beta3/remotesyncer_webhook.go b/internal/webhook/v1beta3/remotesyncer_webhook.go index 5a92b45..374ea58 100644 --- a/internal/webhook/v1beta3/remotesyncer_webhook.go +++ b/internal/webhook/v1beta3/remotesyncer_webhook.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "regexp" + "slices" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -56,13 +57,21 @@ var _ webhook.CustomValidator = &RemoteSyncerCustomValidator{} func validateRemoteSyncerSpec(r *syngitv1beta3.RemoteSyncerSpec) field.ErrorList { var errors field.ErrorList - // Validate DefaultUserBind based on DefaultUnauthorizedUserMode + // Validate DefaultRemoteUserRef based on DefaultUnauthorizedUserMode if r.DefaultUnauthorizedUserMode == syngitv1beta3.Block && r.DefaultRemoteUserRef != nil { errors = append(errors, field.Invalid(field.NewPath("spec").Child("defaultRemoteUserRef"), r.DefaultRemoteUserRef, "should not be set when defaultUnauthorizedUserMode is set to \"Block\"")) } else if r.DefaultUnauthorizedUserMode == syngitv1beta3.UseDefaultUser && r.DefaultRemoteUserRef == nil { errors = append(errors, field.Required(field.NewPath("spec").Child("defaultRemoteUserRef"), "must be set when defaultUnauthorizedUserMode is set to \"UseDefaultUser\"")) } + // Validate DefaultRemoteUserRef and DefaultRemoteTargetRef + if r.DefaultRemoteUserRef != nil && r.DefaultRemoteTargetRef == nil { + errors = append(errors, field.Invalid(field.NewPath("spec").Child("defaultRemoteTargetRef"), r.DefaultRemoteTargetRef, "should be set when defaultRemoteUserRef is set")) + } + if r.DefaultRemoteUserRef == nil && r.DefaultRemoteTargetRef != nil { + errors = append(errors, field.Invalid(field.NewPath("spec").Child("defaultRemoteUserRef"), r.DefaultRemoteUserRef, "should be set when defaultRemoteTargetRef is set")) + } + // Validate DefaultBlockAppliedMessage only exists if Strategy is set to CommitOnly if r.DefaultBlockAppliedMessage != "" && r.Strategy != syngitv1beta3.CommitOnly { errors = append(errors, field.Forbidden(field.NewPath("spec").Child("defaultBlockAppliedMessage"), fmt.Sprintf("should not be set if strategy is not set to \"%s\"", syngitv1beta3.CommitOnly))) @@ -86,16 +95,19 @@ func validateRemoteSyncerSpec(r *syngitv1beta3.RemoteSyncerSpec) field.ErrorList } } - // Validate that DefaultBranch exists if TargetStrategy is set to "SameBranch" - if r.TargetStrategy == syngitv1beta3.SameBranch && r.DefaultBranch == "" { - errors = append(errors, field.Required(field.NewPath("spec").Child("defaultBranch"), "must be set when defaultBranch is set to \"SameBranch\"")) - } - // Validate that DefaultBranch exists if DefaultUnauthorizedUser uses a default user if r.DefaultUnauthorizedUserMode != syngitv1beta3.Block && r.DefaultBranch == "" { errors = append(errors, field.Required(field.NewPath("spec").Child("defaultBranch"), "must be set when the defaultUnauthorizedUserMode is set to UseDefaultUser")) } + // Validate that no namespaces are referenced + if r.DefaultRemoteUserRef != nil && r.DefaultRemoteUserRef.Namespace != "" { + errors = append(errors, field.Invalid(field.NewPath("spec").Child("defaultRemoteUserRef").Child("namespace"), r.DefaultRemoteUserRef.Namespace, "should not be set as it is not supported in this version of syngit")) + } + if r.DefaultRemoteTargetRef != nil && r.DefaultRemoteTargetRef.Namespace != "" { + errors = append(errors, field.Invalid(field.NewPath("spec").Child("defaultRemoteTargetRef").Child("namespace"), r.DefaultRemoteTargetRef.Namespace, "should not be set as it is not supported in this version of syngit")) + } + return errors } @@ -111,6 +123,14 @@ func validateRemoteSyncer(remoteSyncer *syngitv1beta3.RemoteSyncer) error { if err := validateRemoteSyncerSpec(&remoteSyncer.Spec); err != nil { allErrs = append(allErrs, err...) } + + // Validate the TargetPatterns + rtAnnotationUserSpecific := remoteSyncer.Annotations[syngitv1beta3.RtAnnotationUserSpecificKey] + if !slices.Contains([]syngitv1beta3.RemoteTargetUserSpecificValues{"", syngitv1beta3.RtAnnotationOneUserOneBranchValue, syngitv1beta3.RtAnnotationOneUserOneBranchValue}, syngitv1beta3.RemoteTargetUserSpecificValues(rtAnnotationUserSpecific)) { + allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("annotations").Child(syngitv1beta3.RtAnnotationUserSpecificKey), rtAnnotationUserSpecific, + fmt.Sprintf("must be either %s or %s; got %s", string(syngitv1beta3.RtAnnotationOneUserOneBranchValue), string(syngitv1beta3.RtAnnotationOneUserOneBranchValue), rtAnnotationUserSpecific))) + } + if len(allErrs) == 0 { return nil } diff --git a/internal/webhook/v1beta3/remotetarget_webhook.go b/internal/webhook/v1beta3/remotetarget_webhook.go new file mode 100644 index 0000000..bfab411 --- /dev/null +++ b/internal/webhook/v1beta3/remotetarget_webhook.go @@ -0,0 +1,114 @@ +/* +Copyright 2024. + +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 v1beta3 + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + syngitv1beta3 "github.com/syngit-org/syngit/pkg/api/v1beta3" +) + +// nolint:unused +// log is for logging in this package. +var remotetargetlog = logf.Log.WithName("remotetarget-resource") + +// SetupRemoteTargetWebhookWithManager registers the webhook for RemoteTarget in the manager. +func SetupRemoteTargetWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&syngitv1beta3.RemoteTarget{}). + WithValidator(&RemoteTargetCustomValidator{}). + Complete() +} + +// +kubebuilder:webhook:path=/validate-syngit-io-v1beta3-remotetarget,mutating=false,failurePolicy=fail,sideEffects=None,groups=syngit.io,resources=remotetargets,verbs=create;update,versions=v1beta3,name=vremotetarget-v1beta3.kb.io,admissionReviewVersions=v1 + +type RemoteTargetCustomValidator struct { + //TODO(user): Add more fields as needed for validation +} + +var _ webhook.CustomValidator = &RemoteTargetCustomValidator{} + +func validateRemoteTargetSpec(r *syngitv1beta3.RemoteTargetSpec) field.ErrorList { + var errors field.ErrorList + + // Validate MergeStrategy + if r.UpstreamBranch == r.TargetBranch && r.UpstreamRepository == r.TargetRepository && r.MergeStrategy != "" { + errors = append(errors, field.Invalid(field.NewPath("spec").Child("mergeStrategy"), r.MergeStrategy, "should not be set when the target repo & target branch are the same as the upstream repo & branch")) + } + + if (r.UpstreamBranch != r.TargetBranch || r.UpstreamRepository != r.TargetRepository) && r.MergeStrategy == "" { + errors = append(errors, field.Invalid(field.NewPath("spec").Child("mergeStrategy"), r.MergeStrategy, "should be set when the target repo & target branch are different from the upstream repo & branch")) + } + + return errors +} + +func validateRemoteTarget(remoteTarget *syngitv1beta3.RemoteTarget) error { + var allErrs field.ErrorList + if err := validateRemoteTargetSpec(&remoteTarget.Spec); err != nil { + allErrs = append(allErrs, err...) + } + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid( + schema.GroupKind{Group: "syngit.io", Kind: "RemoteTarget"}, + remoteTarget.Name, allErrs) +} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type RemoteTarget. +func (v *RemoteTargetCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + remotetarget, ok := obj.(*syngitv1beta3.RemoteTarget) + if !ok { + return nil, fmt.Errorf("expected a RemoteTarget object but got %T", obj) + } + remotetargetlog.Info("Validation for RemoteTarget upon creation", "name", remotetarget.GetName()) + + return nil, validateRemoteTarget(remotetarget) +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type RemoteTarget. +func (v *RemoteTargetCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + remotetarget, ok := newObj.(*syngitv1beta3.RemoteTarget) + if !ok { + return nil, fmt.Errorf("expected a RemoteTarget object for the newObj but got %T", newObj) + } + remotetargetlog.Info("Validation for RemoteTarget upon update", "name", remotetarget.GetName()) + + return nil, validateRemoteTarget(remotetarget) +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type RemoteTarget. +func (v *RemoteTargetCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + remotetarget, ok := obj.(*syngitv1beta3.RemoteTarget) + if !ok { + return nil, fmt.Errorf("expected a RemoteTarget object but got %T", obj) + } + remotetargetlog.Info("Validation for RemoteTarget upon deletion", "name", remotetarget.GetName()) + + return nil, nil +} diff --git a/internal/webhook/v1beta3/remotetarget_webhook_test.go b/internal/webhook/v1beta3/remotetarget_webhook_test.go new file mode 100644 index 0000000..c5e7fd1 --- /dev/null +++ b/internal/webhook/v1beta3/remotetarget_webhook_test.go @@ -0,0 +1,81 @@ +/* +Copyright 2024. + +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 v1beta3 + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + syngitv1beta3 "github.com/syngit-org/syngit/pkg/api/v1beta3" + // TODO (user): Add any additional imports if needed +) + +var _ = Describe("RemoteTarget Webhook", func() { + var ( + obj *syngitv1beta3.RemoteTarget + oldObj *syngitv1beta3.RemoteTarget + validator RemoteTargetCustomValidator + ) + + BeforeEach(func() { + obj = &syngitv1beta3.RemoteTarget{} + oldObj = &syngitv1beta3.RemoteTarget{} + validator = RemoteTargetCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") + Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") + // TODO (user): Add any setup logic common to all tests + }) + + AfterEach(func() { + // TODO (user): Add any teardown logic common to all tests + }) + + Context("When creating or updating RemoteTarget under Validating Webhook", func() { + // TODO (user): Add logic for validating webhooks + // Example: + // It("Should deny creation if a required field is missing", func() { + // By("simulating an invalid creation scenario") + // obj.SomeRequiredField = "" + // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) + // }) + // + // It("Should admit creation if all required fields are present", func() { + // By("simulating an invalid creation scenario") + // obj.SomeRequiredField = "valid_value" + // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) + // }) + // + // It("Should validate updates correctly", func() { + // By("simulating a valid update scenario") + // oldObj.SomeRequiredField = "updated_value" + // obj.SomeRequiredField = "updated_value" + // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) + // }) + }) + + Context("When creating RemoteTarget under Conversion Webhook", func() { + // TODO (user): Add logic to convert the object to the desired version and verify the conversion + // Example: + // It("Should convert the object correctly", func() { + // convertedObj := &syngitv1beta3.RemoteTarget{} + // Expect(obj.ConvertTo(convertedObj)).To(Succeed()) + // Expect(convertedObj).ToNot(BeNil()) + // }) + }) + +}) diff --git a/internal/webhook/v1beta3/remoteuser_association_webhook.go b/internal/webhook/v1beta3/remoteuser_association_webhook.go deleted file mode 100644 index d494654..0000000 --- a/internal/webhook/v1beta3/remoteuser_association_webhook.go +++ /dev/null @@ -1,233 +0,0 @@ -package v1beta3 - -import ( - "context" - "fmt" - "net/http" - "strings" - - syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" - corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" -) - -/* - Handle webhook and get kubernetes user id -*/ - -type RemoteUserAssociationWebhookHandler struct { - Client client.Client - Decoder *admission.Decoder -} - -const ( - managedByLabelKey = "managed-by" - managedByLabelValue = "syngit.io" - k8sUserLabelKey = "syngit.io/k8s-user" -) - -// +kubebuilder:webhook:path=/syngit-v1beta3-remoteuser-association,mutating=false,failurePolicy=fail,sideEffects=None,groups=syngit.io,resources=remoteusers,verbs=create;update;delete,versions=v1beta3,admissionReviewVersions=v1,name=vremoteusers-association.v1beta3.syngit.io - -func (ruwh *RemoteUserAssociationWebhookHandler) Handle(ctx context.Context, req admission.Request) admission.Response { - - username := req.DeepCopy().UserInfo.Username - name := syngit.RubPrefix + username - - rubs := &syngit.RemoteUserBindingList{} - listOps := &client.ListOptions{ - LabelSelector: labels.SelectorFromSet(labels.Set{ - managedByLabelKey: managedByLabelValue, - k8sUserLabelKey: username, - }), - Namespace: req.Namespace, - } - rubErr := ruwh.Client.List(ctx, rubs, listOps) - if rubErr != nil { - return admission.Errored(http.StatusInternalServerError, rubErr) - } - - if len(rubs.Items) > 1 { - return admission.Denied(fmt.Sprintf("only one RemoteUserBinding for the user %s should be managed by Syngit", username)) - } - - if string(req.Operation) == "DELETE" { //nolint:goconst - if len(rubs.Items) <= 0 { - return admission.Allowed("This object was not associated with any RemoteUserBinding") - } else { - rub := rubs.Items[0] - return ruwh.removeRuFromRub(ctx, req, &rub) - } - } - - ru := &syngit.RemoteUser{} - err := ruwh.Decoder.Decode(req, ru) - if err != nil { - return admission.Errored(http.StatusBadRequest, err) - } - objRef := corev1.ObjectReference{Name: ru.Name} - - isAlreadyDefined, rubFoundName, definedErr := ruwh.isAlreadyReferenced(ctx, req.Name, req.Namespace) - if definedErr != nil { - return admission.Errored(http.StatusInternalServerError, definedErr) - } - - if len(rubs.Items) <= 0 { - - if ru.Annotations[syngit.RubAnnotation] == "" || ru.Annotations[syngit.RubAnnotation] == "false" { - return admission.Allowed("This object is not associated with any RemoteUserBinding") - } - - // Geneate the RUB object with the right name - rub, generateErr := ruwh.generateRemoteUserBinding(ctx, name, req.Namespace, 0) - if generateErr != nil { - return admission.Errored(http.StatusInternalServerError, generateErr) - } - if isAlreadyDefined && name != rubFoundName { - return admission.Denied(fmt.Sprintf("the RemoteUser is already bound in the RemoteUserBinding %s", rubFoundName)) - } - - // Create the RemoteUserBinding object - rub.Namespace = req.Namespace - - // Set the labels - rub.Labels = map[string]string{ - managedByLabelKey: managedByLabelValue, - k8sUserLabelKey: username, - } - - subject := &rbacv1.Subject{ - Kind: "User", - Name: username, - } - rub.Spec.Subject = *subject - - remoteRefs := make([]corev1.ObjectReference, 0) - remoteRefs = append(remoteRefs, objRef) - rub.Spec.RemoteUserRefs = remoteRefs - - createErr := ruwh.Client.Create(ctx, rub) - if createErr != nil { - return admission.Errored(http.StatusInternalServerError, createErr) - } - } else { - // The RemoteUserBinding already exists - - rub := rubs.Items[0] - name = rub.Name - - if isAlreadyDefined && name != rubFoundName { - return admission.Denied(fmt.Sprintf("the RemoteUser is already bound in the RemoteUserBinding %s", rubFoundName)) - } - if ru.Annotations[syngit.RubAnnotation] == "" || ru.Annotations[syngit.RubAnnotation] == "false" { - return ruwh.removeRuFromRub(ctx, req, &rub) - } - - dontAppend := false - remoteRefs := rub.DeepCopy().Spec.RemoteUserRefs - for _, ruRef := range remoteRefs { - if ruRef.Name == ru.Name { - dontAppend = true - } - } - if !dontAppend { - remoteRefs = append(remoteRefs, objRef) - } - rub.Spec.RemoteUserRefs = remoteRefs - - updateErr := ruwh.Client.Update(ctx, &rub) - if updateErr != nil { - return admission.Errored(http.StatusInternalServerError, updateErr) - } - - } - - return admission.Allowed("This object is associated to the " + name + " RemoteUserBinding") -} - -func (ruwh *RemoteUserAssociationWebhookHandler) isAlreadyReferenced(ctx context.Context, ruName string, ruNamespace string) (bool, string, error) { - rubs := &syngit.RemoteUserBindingList{} - listOps := &client.ListOptions{ - Namespace: ruNamespace, - } - rubErr := ruwh.Client.List(ctx, rubs, listOps) - if rubErr != nil { - return false, "", rubErr - } - - for _, rub := range rubs.Items { - for _, ru := range rub.Spec.RemoteUserRefs { - if ru.Name == ruName { - if ru.Namespace == "" || (ru.Namespace == ruNamespace) { - return true, rub.Name, nil - } - } - } - } - - return false, "", nil -} - -func (ruwh *RemoteUserAssociationWebhookHandler) generateRemoteUserBinding(ctx context.Context, name string, namespace string, suffixNumber int) (*syngit.RemoteUserBinding, error) { - // The RemoteUserBinding does not exists yet - rub := &syngit.RemoteUserBinding{} - - newName := name - if suffixNumber > 0 { - newName = fmt.Sprintf("%s-%d", name, suffixNumber) - } - webhookNamespacedName := &types.NamespacedName{ - Name: newName, - Namespace: namespace, - } - rubErr := ruwh.Client.Get(ctx, *webhookNamespacedName, rub) - if rubErr == nil { - return ruwh.generateRemoteUserBinding(ctx, name, namespace, suffixNumber+1) - } else { - if strings.Contains(rubErr.Error(), "not found") { - rub.Name = newName - rub.Namespace = namespace - return rub, nil - } - return nil, rubErr - } -} - -func (ruwh *RemoteUserAssociationWebhookHandler) removeRuFromRub(ctx context.Context, req admission.Request, rub *syngit.RemoteUserBinding) admission.Response { - name := rub.DeepCopy().Name - - ru := &syngit.RemoteUser{} - err := ruwh.Decoder.DecodeRaw(req.OldObject, ru) - if err != nil { - return admission.Errored(http.StatusBadRequest, err) - } - - remoteRefs := rub.Spec.DeepCopy().RemoteUserRefs - newRemoteRefs := []corev1.ObjectReference{} - for _, rm := range remoteRefs { - if rm.Name != ru.Name { - newRemoteRefs = append(newRemoteRefs, rm) - } - } - - if len(newRemoteRefs) != 0 { - - rub.Spec.RemoteUserRefs = newRemoteRefs - deleteErr := ruwh.Client.Update(ctx, rub) - if deleteErr != nil { - return admission.Errored(http.StatusInternalServerError, deleteErr) - } - - } else { - - deleteErr := ruwh.Client.Delete(ctx, rub) - if deleteErr != nil { - return admission.Errored(http.StatusInternalServerError, deleteErr) - } - } - - return admission.Allowed("This object is not associated with the " + name + " RemoteUserBinding anymore") -} diff --git a/internal/webhook/v1beta3/remoteuser_pattern_association_webhook.go b/internal/webhook/v1beta3/remoteuser_pattern_association_webhook.go new file mode 100644 index 0000000..746cd3d --- /dev/null +++ b/internal/webhook/v1beta3/remoteuser_pattern_association_webhook.go @@ -0,0 +1,144 @@ +package v1beta3 + +import ( + "context" + "net/http" + + patterns "github.com/syngit-org/syngit/internal/patterns/v1beta3" + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +/* + Handle webhook and get kubernetes user id +*/ + +type RemoteUserAssociationWebhookHandler struct { + Client client.Client + Decoder *admission.Decoder +} + +// +kubebuilder:webhook:path=/syngit-v1beta3-remoteuser-association,mutating=false,failurePolicy=fail,sideEffects=None,groups=syngit.io,resources=remoteusers,verbs=create;update;delete,versions=v1beta3,admissionReviewVersions=v1,name=vremoteusers-association.v1beta3.syngit.io + +func (ruwh *RemoteUserAssociationWebhookHandler) Handle(ctx context.Context, req admission.Request) admission.Response { + + var remoteUser *syngit.RemoteUser + var isEnabled = false + + if string(req.Operation) == "DELETE" { //nolint:goconst + remoteUser = &syngit.RemoteUser{} + err := ruwh.Decoder.DecodeRaw(req.OldObject, remoteUser) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + } else { + remoteUser = &syngit.RemoteUser{} + err := ruwh.Decoder.Decode(req, remoteUser) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + if remoteUser.Annotations[syngit.RubAnnotation] == "true" { + isEnabled = true + } + } + + username := req.DeepCopy().UserInfo.Username + associationPattern := &patterns.RemoteUserAssociationPattern{ + PatternSpecification: patterns.PatternSpecification{ + Client: ruwh.Client, + NamespacedName: types.NamespacedName{Name: req.Name, Namespace: req.Namespace}, + }, + Username: username, + RemoteUser: *remoteUser, + IsEnabled: isEnabled, + } + remoteTargetPattern := &patterns.RemoteUserSearchRemoteTargetPattern{ + PatternSpecification: patterns.PatternSpecification{ + Client: ruwh.Client, + NamespacedName: types.NamespacedName{Name: req.Name, Namespace: req.Namespace}, + }, + Username: username, + RemoteUser: *remoteUser, + IsEnabled: isEnabled, + } + + err := patterns.Trigger(associationPattern, ctx) + if err != nil { + if err.Reason == patterns.Denied { + return admission.Denied(err.Message) + } + if err.Reason == patterns.Errored { + return admission.Errored(http.StatusInternalServerError, err) + } + } + + err = patterns.Trigger(remoteTargetPattern, ctx) + if err != nil { + if err.Reason == patterns.Denied { + return admission.Denied(err.Message) + } + if err.Reason == patterns.Errored { + return admission.Errored(http.StatusInternalServerError, err) + } + } + + userSpecificError := ruwh.triggerUserSpecificPatterns(ctx, req, username, remoteTargetPattern) + if userSpecificError != nil { + if userSpecificError.Reason == patterns.Denied { + return admission.Denied(userSpecificError.Message) + } + if userSpecificError.Reason == patterns.Errored { + return admission.Errored(http.StatusInternalServerError, userSpecificError) + } + } + + return admission.Allowed("This object is associated to the " + req.Name + " RemoteUserBinding") +} + +func (ruwh *RemoteUserAssociationWebhookHandler) triggerUserSpecificPatterns(ctx context.Context, req admission.Request, username string, pattern *patterns.RemoteUserSearchRemoteTargetPattern) *patterns.ErrorPattern { + // Get all RemoteSyncer of the namespace that implement the user specific pattern + remoteSyncerList := &syngit.RemoteSyncerList{} + selector := labels.NewSelector() + userSpecificKey, reqErr := labels.NewRequirement(syngit.RtAnnotationUserSpecificKey, selection.Exists, nil) + if reqErr != nil { + return &patterns.ErrorPattern{Message: reqErr.Error(), Reason: patterns.Errored} + } + managedBy, reqErr := labels.NewRequirement(syngit.RtAnnotationUserSpecificKey, selection.Equals, []string{syngit.ManagedByLabelValue}) + if reqErr != nil { + return &patterns.ErrorPattern{Message: reqErr.Error(), Reason: patterns.Errored} + } + selector.Add(*userSpecificKey) + selector.Add(*managedBy) + listOps := &client.ListOptions{ + LabelSelector: selector, + Namespace: req.Namespace, + } + listErr := ruwh.Client.List(ctx, remoteSyncerList, listOps) + if listErr != nil { + return &patterns.ErrorPattern{Message: listErr.Error(), Reason: patterns.Errored} + } + + for _, rsy := range remoteSyncerList.Items { + userSpecificPattern := &patterns.UserSpecificPattern{ + PatternSpecification: patterns.PatternSpecification{ + Client: ruwh.Client, + NamespacedName: types.NamespacedName{Name: req.Name, Namespace: req.Namespace}, + }, + Username: username, + RemoteSyncer: rsy, + RemoteUserBinding: pattern.RemoteUserBinding, + } + + err := patterns.Trigger(userSpecificPattern, ctx) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/webhook/v1beta3/remoteuserbinding_webhook.go b/internal/webhook/v1beta3/remoteuserbinding_webhook.go index ce81384..a1f7476 100644 --- a/internal/webhook/v1beta3/remoteuserbinding_webhook.go +++ b/internal/webhook/v1beta3/remoteuserbinding_webhook.go @@ -17,8 +17,17 @@ limitations under the License. package v1beta3 import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" syngitv1beta3 "github.com/syngit-org/syngit/pkg/api/v1beta3" ) @@ -30,7 +39,80 @@ var remoteuserbindinglog = logf.Log.WithName("remoteuserbinding-resource") // SetupRemoteUserBindingWebhookWithManager registers the webhook for RemoteUserBinding in the manager. func SetupRemoteUserBindingWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr).For(&syngitv1beta3.RemoteUserBinding{}). + WithValidator(&RemoteUserBindingCustomValidator{}). Complete() } -// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// +kubebuilder:webhook:path=/validate-syngit-io-v1beta3-remoteuserbinding,mutating=false,failurePolicy=fail,sideEffects=None,groups=syngit.io,resources=remoteuserbindings,verbs=create;update,versions=v1beta3,name=vremoteuserbinding-v1beta3.kb.io,admissionReviewVersions=v1 + +type RemoteUserBindingCustomValidator struct { +} + +var _ webhook.CustomValidator = &RemoteUserBindingCustomValidator{} + +// Validate validates the RemoteSyncerSpec +func validateRemoteUserBindingSpec(r *syngitv1beta3.RemoteUserBindingSpec) field.ErrorList { + var errors field.ErrorList + + // Validate that no namespaces are referenced + for i, remoteUserRef := range r.RemoteUserRefs { + if remoteUserRef.Namespace != "" { + errors = append(errors, field.Invalid(field.NewPath("spec").Child("remoteUserRefs").Index(i), r.RemoteUserRefs[i].Namespace, "should not be set as it is not supported in this version of syngit")) + } + } + for i, remoteTargetRef := range r.RemoteTargetRefs { + if remoteTargetRef.Namespace != "" { + errors = append(errors, field.Invalid(field.NewPath("spec").Child("remoteTargetRefs").Index(i), r.RemoteTargetRefs[i].Namespace, "should not be set as it is not supported in this version of syngit")) + } + } + + return errors +} + +func validateRemoteUserBinding(remoteUserBinding *syngitv1beta3.RemoteUserBinding) error { + var allErrs field.ErrorList + if err := validateRemoteUserBindingSpec(&remoteUserBinding.Spec); err != nil { + allErrs = append(allErrs, err...) + } + + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid( + schema.GroupKind{Group: "syngit.io", Kind: "RemoteUserBinding"}, + remoteUserBinding.Name, allErrs) +} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type RemoteUserBinding. +func (v *RemoteUserBindingCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + remoteuserbinding, ok := obj.(*syngitv1beta3.RemoteUserBinding) + if !ok { + return nil, fmt.Errorf("expected a RemoteUserBinding object but got %T", obj) + } + remoteuserbindinglog.Info("Validation for RemoteUserBinding upon creation", "name", remoteuserbinding.GetName()) + + return nil, validateRemoteUserBinding(remoteuserbinding) +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type RemoteUserBinding. +func (v *RemoteUserBindingCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + remoteuserbinding, ok := newObj.(*syngitv1beta3.RemoteUserBinding) + if !ok { + return nil, fmt.Errorf("expected a RemoteUserBinding object for the newObj but got %T", newObj) + } + remoteuserbindinglog.Info("Validation for RemoteUserBinding upon update", "name", remoteuserbinding.GetName()) + + return nil, validateRemoteUserBinding(remoteuserbinding) +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type RemoteUserBinding. +func (v *RemoteUserBindingCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + remoteuserbinding, ok := obj.(*syngitv1beta3.RemoteUserBinding) + if !ok { + return nil, fmt.Errorf("expected a RemoteUserBinding object but got %T", obj) + } + remoteuserbindinglog.Info("Validation for RemoteUserBinding upon deletion", "name", remoteuserbinding.GetName()) + + return nil, nil +} diff --git a/internal/webhook/v1beta3/webhook_suite_test.go b/internal/webhook/v1beta3/webhook_suite_test.go index 21687cc..3b7fd1e 100644 --- a/internal/webhook/v1beta3/webhook_suite_test.go +++ b/internal/webhook/v1beta3/webhook_suite_test.go @@ -32,7 +32,6 @@ import ( admissionv1 "k8s.io/api/admission/v1" syngitv1beta3 "github.com/syngit-org/syngit/pkg/api/v1beta3" - // +kubebuilder:scaffold:imports apimachineryruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" @@ -124,6 +123,9 @@ var _ = BeforeSuite(func() { err = SetupRemoteUserWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) + err = SetupRemoteTargetWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + // +kubebuilder:scaffold:webhook go func() { diff --git a/pkg/api/v1beta1/remotesyncer_conversion.go b/pkg/api/v1beta1/remotesyncer_conversion.go index 5a6cea6..01fbc39 100644 --- a/pkg/api/v1beta1/remotesyncer_conversion.go +++ b/pkg/api/v1beta1/remotesyncer_conversion.go @@ -38,9 +38,19 @@ func (src *RemoteSyncer) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.RootPath = src.Spec.RootPath dst.Spec.ScopedResources = v1beta3.ScopedResources(src.Spec.ScopedResources) dst.Spec.Strategy = v1beta3.Strategy(src.Spec.ProcessMode) - dst.Spec.TargetStrategy = v1beta3.TargetStrategy(src.Spec.PushMode) dst.Spec.CABundleSecretRef = src.Spec.CABundleSecretRef + // Target transfer + if dst.Annotations == nil { + dst.Annotations = map[string]string{} + } + if string(src.Spec.PushMode) == string(SameBranch) { + dst.Spec.TargetStrategy = v1beta3.OneTarget + dst.Annotations[v1beta3.RtAnnotationOneOrManyBranchesKey] = src.Spec.DefaultBranch + } else { + dst.Spec.TargetStrategy = v1beta3.MultipleTarget + } + return nil } @@ -61,8 +71,14 @@ func (dst *RemoteSyncer) ConvertFrom(srcRaw conversion.Hub) error { dst.Spec.RootPath = src.Spec.RootPath dst.Spec.ScopedResources = ScopedResources(src.Spec.ScopedResources) dst.Spec.ProcessMode = ProcessMode(src.Spec.Strategy) - dst.Spec.PushMode = PushMode(src.Spec.TargetStrategy) dst.Spec.CABundleSecretRef = src.Spec.CABundleSecretRef + // Target transfer + if src.Spec.TargetStrategy == v1beta3.OneTarget { + dst.Spec.PushMode = SameBranch + } else { + dst.Spec.PushMode = MultipleBranch + } + return nil } diff --git a/pkg/api/v1beta1/remoteuser_conversion.go b/pkg/api/v1beta1/remoteuser_conversion.go index f34d53b..6875d8e 100644 --- a/pkg/api/v1beta1/remoteuser_conversion.go +++ b/pkg/api/v1beta1/remoteuser_conversion.go @@ -41,10 +41,12 @@ func (src *RemoteUser) ConvertTo(dstRaw conversion.Hub) error { dst.Status.LastAuthTime = src.Status.LastAuthTime dst.Status.SecretBoundStatus = v1beta3.SecretBoundStatus(src.Status.SecretBoundStatus) - // Renaming - + // Annotation transfer + if dst.Annotations == nil { + dst.Annotations = map[string]string{} + } associatedRemoteUserBinding := strconv.FormatBool(src.Spec.AssociatedRemoteUserBinding) - dst.Annotations["syngit.io/associated-remote-userbinding"] = associatedRemoteUserBinding + dst.Annotations[v1beta3.RubAnnotation] = associatedRemoteUserBinding return nil } @@ -68,8 +70,7 @@ func (dst *RemoteUser) ConvertFrom(srcRaw conversion.Hub) error { dst.Status.SecretBoundStatus = SecretBoundStatus(src.Status.SecretBoundStatus) // Renaming - - associatedRemoteUserBinding, err := strconv.ParseBool(src.Annotations["syngit.io/associated-remote-userbinding"]) + associatedRemoteUserBinding, err := strconv.ParseBool(src.Annotations[v1beta3.RubAnnotation]) if err != nil { dst.Spec.AssociatedRemoteUserBinding = false } else { diff --git a/pkg/api/v1beta1/remoteuserbinding_types.go b/pkg/api/v1beta1/remoteuserbinding_types.go index b3b94fc..56c4a0c 100644 --- a/pkg/api/v1beta1/remoteuserbinding_types.go +++ b/pkg/api/v1beta1/remoteuserbinding_types.go @@ -22,10 +22,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -const ( - RubPrefix = "associated-rub-" -) - type RemoteUserBindingSpec struct { Subject rbacv1.Subject `json:"subject"` RemoteRefs []corev1.ObjectReference `json:"remoteRefs"` // Ref to the listed RemoteUser objects diff --git a/pkg/api/v1beta2/remotesyncer_conversion.go b/pkg/api/v1beta2/remotesyncer_conversion.go index b0c1f58..4797c08 100644 --- a/pkg/api/v1beta2/remotesyncer_conversion.go +++ b/pkg/api/v1beta2/remotesyncer_conversion.go @@ -38,9 +38,19 @@ func (src *RemoteSyncer) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.RootPath = src.Spec.RootPath dst.Spec.ScopedResources = v1beta3.ScopedResources(src.Spec.ScopedResources) dst.Spec.Strategy = v1beta3.Strategy(src.Spec.ProcessMode) - dst.Spec.TargetStrategy = v1beta3.TargetStrategy(src.Spec.PushMode) dst.Spec.CABundleSecretRef = src.Spec.CABundleSecretRef + // Target transfer + if dst.Annotations == nil { + dst.Annotations = map[string]string{} + } + if string(src.Spec.PushMode) == string(SameBranch) { + dst.Spec.TargetStrategy = v1beta3.OneTarget + dst.Annotations[v1beta3.RtAnnotationOneOrManyBranchesKey] = src.Spec.DefaultBranch + } else { + dst.Spec.TargetStrategy = v1beta3.MultipleTarget + } + return nil } @@ -61,8 +71,14 @@ func (dst *RemoteSyncer) ConvertFrom(srcRaw conversion.Hub) error { dst.Spec.RootPath = src.Spec.RootPath dst.Spec.ScopedResources = ScopedResources(src.Spec.ScopedResources) dst.Spec.ProcessMode = ProcessMode(src.Spec.Strategy) - dst.Spec.PushMode = PushMode(src.Spec.TargetStrategy) dst.Spec.CABundleSecretRef = src.Spec.CABundleSecretRef + // Target transfer + if src.Spec.TargetStrategy == v1beta3.OneTarget { + dst.Spec.PushMode = SameBranch + } else { + dst.Spec.PushMode = MultipleBranch + } + return nil } diff --git a/pkg/api/v1beta2/remoteuser_conversion.go b/pkg/api/v1beta2/remoteuser_conversion.go index e762ae8..ad3946a 100644 --- a/pkg/api/v1beta2/remoteuser_conversion.go +++ b/pkg/api/v1beta2/remoteuser_conversion.go @@ -39,6 +39,13 @@ func (src *RemoteUser) ConvertTo(dstRaw conversion.Hub) error { dst.Status.LastAuthTime = src.Status.LastAuthTime dst.Status.SecretBoundStatus = v1beta3.SecretBoundStatus(src.Status.SecretBoundStatus) + // Annotation transfer + if dst.Annotations == nil { + dst.Annotations = map[string]string{} + } + dst.Annotations[v1beta3.RubAnnotation] = src.Annotations[RubAnnotation] + dst.Annotations[RubAnnotation] = "" + return nil } @@ -60,5 +67,12 @@ func (dst *RemoteUser) ConvertFrom(srcRaw conversion.Hub) error { dst.Status.LastAuthTime = src.Status.LastAuthTime dst.Status.SecretBoundStatus = SecretBoundStatus(src.Status.SecretBoundStatus) + // Annotation transfer + if dst.Annotations == nil { + dst.Annotations = map[string]string{} + } + dst.Annotations[RubAnnotation] = src.Annotations[v1beta3.RubAnnotation] + dst.Annotations[v1beta3.RubAnnotation] = "" + return nil } diff --git a/pkg/api/v1beta3/groupversion_info.go b/pkg/api/v1beta3/groupversion_info.go index 9ad667d..efc0d1f 100644 --- a/pkg/api/v1beta3/groupversion_info.go +++ b/pkg/api/v1beta3/groupversion_info.go @@ -34,3 +34,9 @@ var ( // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme ) + +const ( + ManagedByLabelKey = "managed-by" + ManagedByLabelValue = "syngit.io" + K8sUserLabelKey = "syngit.io/k8s-user" +) diff --git a/pkg/api/v1beta3/remotesyncer_types.go b/pkg/api/v1beta3/remotesyncer_types.go index 8344506..d4cd287 100644 --- a/pkg/api/v1beta3/remotesyncer_types.go +++ b/pkg/api/v1beta3/remotesyncer_types.go @@ -34,8 +34,9 @@ type RemoteSyncerSpec struct { RemoteRepository string `json:"remoteRepository" protobuf:"bytes,1,name=remoteRepository"` // +kubebuilder:example="main" - // +kubebuilder:validation:Optional - DefaultBranch string `json:"defaultBranch,omitempty" protobuf:"bytes,opt,2,name=defaultBranch"` + // +kubebuilder:default:value="main" + // +kubebuilder:validation:Required + DefaultBranch string `json:"defaultBranch" protobuf:"bytes,opt,2,name=defaultBranch"` // +kubebuilder:default:value={} // +kubebuilder:validation:Required @@ -47,40 +48,46 @@ type RemoteSyncerSpec struct { Strategy Strategy `json:"strategy" protobuf:"bytes,4,name=strategy"` // +kubebuilder:validation:Required - // +kubebuilder:default:value="SameBranch" - // +kubebuilder:validation:Enum=SameBranch;MultipleBranch;Fork + // +kubebuilder:default:value="OneTarget" + // +kubebuilder:validation:Enum=OneTarget;MultipleTarget TargetStrategy TargetStrategy `json:"targetStrategy" protobuf:"bytes,5,name=targetStrategy"` // +kubebuilder:validation:Optional - DefaultBlockAppliedMessage string `json:"defaultBlockAppliedMessage,omitempty" protobuf:"bytes,opt,6,name=defaultBlockAppliedMessage"` + RemoteTargetSelector *metav1.LabelSelector `json:"remoteTargetSelector" protobuf:"bytes,opt,6,name=remoteTargetSelector"` // +kubebuilder:validation:Optional - ExcludedFields []string `json:"excludedFields,omitempty" protobuf:"bytes,opt,7,name=excludedFields"` + DefaultBlockAppliedMessage string `json:"defaultBlockAppliedMessage,omitempty" protobuf:"bytes,opt,7,name=defaultBlockAppliedMessage"` // +kubebuilder:validation:Optional - ExcludedFieldsConfigMapRef *corev1.ObjectReference `json:"excludedFieldsConfig,omitempty" protobuf:"bytes,opt,8,name=excludedFieldsConfig"` // Ref to a ConfigMap + ExcludedFields []string `json:"excludedFields,omitempty" protobuf:"bytes,opt,8,name=excludedFields"` // +kubebuilder:validation:Optional - RootPath string `json:"rootPath,omitempty" protobuf:"bytes,opt,9,name=rootPath"` + ExcludedFieldsConfigMapRef *corev1.ObjectReference `json:"excludedFieldsConfig,omitempty" protobuf:"bytes,opt,9,name=excludedFieldsConfig"` // Ref to a ConfigMap // +kubebuilder:validation:Optional - RemoteUserBindingSelector *metav1.LabelSelector `json:"remoteUserBindingSelector" protobuf:"bytes,opt,10,name=remoteUserBindingSelector"` + RootPath string `json:"rootPath,omitempty" protobuf:"bytes,opt,10,name=rootPath"` // +kubebuilder:validation:Optional - BypassInterceptionSubjects []rbacv1.Subject `json:"bypassInterceptionSubjects,omitempty" protobuf:"bytes,opt,11,name=bypassInterceptionSubjects"` + RemoteUserBindingSelector *metav1.LabelSelector `json:"remoteUserBindingSelector" protobuf:"bytes,opt,11,name=remoteUserBindingSelector"` + + // +kubebuilder:validation:Optional + BypassInterceptionSubjects []rbacv1.Subject `json:"bypassInterceptionSubjects,omitempty" protobuf:"bytes,opt,12,name=bypassInterceptionSubjects"` // +kubebuilder:default:value="Block" // +kubebuilder:validation:Enum=Block;UseDefaultUser - DefaultUnauthorizedUserMode DefaultUnauthorizedUserMode `json:"defaultUnauthorizedUserMode" protobuf:"bytes,opt,12,name=defaultUnauthorizedUserMode"` + DefaultUnauthorizedUserMode DefaultUnauthorizedUserMode `json:"defaultUnauthorizedUserMode" protobuf:"bytes,opt,13,name=defaultUnauthorizedUserMode"` + + // +kubebuilder:validation:Optional + DefaultRemoteUserRef *corev1.ObjectReference `json:"defaultRemoteUserRef,omitempty" protobuf:"bytes,opt,14,name=defaultRemoteUserRef"` // Ref to a RemoteUser object // +kubebuilder:validation:Optional - DefaultRemoteUserRef *corev1.ObjectReference `json:"defaultRemoteUserRef,omitempty" protobuf:"bytes,opt,13,name=defaultRemoteUserRef"` // Ref to a RemoteUser object + DefaultRemoteTargetRef *corev1.ObjectReference `json:"defaultRemoteTargetRef,omitempty" protobuf:"bytes,opt,15,name=defaultRemoteTargetRef"` // Ref to a RemoteUser object // +kubebuilder:validation:Optional - InsecureSkipTlsVerify bool `json:"insecureSkipTlsVerify,omitempty" protobuf:"bytes,opt,14,name=insecureSkipTlsVerify"` + InsecureSkipTlsVerify bool `json:"insecureSkipTlsVerify,omitempty" protobuf:"bytes,opt,16,name=insecureSkipTlsVerify"` // +kubebuilder:validation:Optional - CABundleSecretRef corev1.SecretReference `json:"caBundleSecretRef,omitempty" protobuf:"bytes,opt,15,name=caBundleSecretRef"` + CABundleSecretRef corev1.SecretReference `json:"caBundleSecretRef,omitempty" protobuf:"bytes,opt,17,name=caBundleSecretRef"` } type RemoteSyncerStatus struct { @@ -93,13 +100,13 @@ type RemoteSyncerStatus struct { Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` // +optional - LastBypassedObjectState LastBypassedObjectState `json:"lastBypassedObjectState,omitempty"` + LastBypassedObjectState LastBypassedObjectState `json:"lastBypassedObjectState,omitempty" protobuf:"bytes,2,rep,name=lastBypassedObjectState"` // +optional - LastObservedObjectState LastObservedObjectState `json:"lastObservedObjectState,omitempty"` + LastObservedObjectState LastObservedObjectState `json:"lastObservedObjectState,omitempty" protobuf:"bytes,3,rep,name=lastObservedObjectState"` // +optional - LastPushedObjectState LastPushedObjectState `json:"lastPushedObjectState,omitempty"` + LastPushedObjectState LastPushedObjectState `json:"lastPushedObjectState,omitempty" protobuf:"bytes,4,rep,name=lastPushedObjectState"` } //+kubebuilder:object:root=true @@ -136,9 +143,15 @@ func init() { type TargetStrategy string const ( - SameBranch TargetStrategy = "SameBranch" - MultipleBranch TargetStrategy = "MultipleBranch" - Fork TargetStrategy = "Fork" + OneTarget TargetStrategy = "OneTarget" + MultipleTarget TargetStrategy = "MultipleTarget" +) + +type TargetPattern string + +const ( + OneUserOneBranch TargetPattern = "OneUserOneBranch" + OneOrMultipleBranches TargetPattern = "OneOrMultipleBranches" ) type Strategy string @@ -237,13 +250,13 @@ type LastPushedObjectState struct { LastPushedGitUser string `json:"lastPushedGitUser,omitempty"` // +optional - LastPushedObjectGitRepo string `json:"lastPushedObjectGitRepo,omitempty"` + LastPushedObjectGitRepos []string `json:"lastPushedObjectGitRepo,omitempty"` // +optional LastPushedObjectGitPath string `json:"lastPushedObjectGitPath,omitempty"` // +optional - LastPushedObjectGitCommitHash string `json:"lastPushedObjectCommitHash,omitempty"` + LastPushedObjectGitCommitHashes []string `json:"lastPushedObjectCommitHash,omitempty"` // +optional LastPushedObject JsonGVRN `json:"lastPushedObject,omitempty"` diff --git a/pkg/api/v1beta3/remotetarget_conversion.go b/pkg/api/v1beta3/remotetarget_conversion.go new file mode 100644 index 0000000..cbc0ce0 --- /dev/null +++ b/pkg/api/v1beta3/remotetarget_conversion.go @@ -0,0 +1,19 @@ +/* +Copyright 2024. + +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 v1beta3 + +func (*RemoteTarget) Hub() {} diff --git a/pkg/api/v1beta3/remotetarget_types.go b/pkg/api/v1beta3/remotetarget_types.go new file mode 100644 index 0000000..1c92117 --- /dev/null +++ b/pkg/api/v1beta3/remotetarget_types.go @@ -0,0 +1,123 @@ +/* +Copyright 2024. + +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 v1beta3 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + RtAnnotationUserSpecificKey = "syngit.io/remotetarget.pattern.user-specific" + RtAnnotationOneOrManyBranchesKey = "syngit.io/remotetarget.pattern.one-or-many-branches" + RtAllowInjection = "syngit.io/remotetarget.allow-injection" + RtPrefix = "rt" + RtLabelBranchKey = "syngit.io/remotetarget.branch" + RtLabelPatternKey = "syngit.io/remotetarget.pattern" + RtLabelOneOrManyBranchesValue = "one-or-many-branches" + RtDefaultForkName = "fork" +) + +type RemoteTargetUserSpecificValues string + +const ( + RtAnnotationOneUserOneBranchValue RemoteTargetUserSpecificValues = "one-user-one-branch" + RtAnnotationOneUserOneForkValue RemoteTargetUserSpecificValues = "one-user-one-fork" +) + +// RemoteTargetSpec defines the desired state of RemoteTarget. +type RemoteTargetSpec struct { + + // +kubebuilder:validation:Required + // +kubebuilder:example="https://git.example.com/my-upstream-repo.git" + // +kubebuilder:validation:Format=uri + UpstreamRepository string `json:"upstreamRepository" protobuf:"bytes,1,name=upstreamRepository"` + + // +kubebuilder:validation:Required + // +kubebuilder:example:"main" + UpstreamBranch string `json:"upstreamBranch" protobuf:"bytes,2,name=upstreamBranch"` + + // +kubebuilder:validation:Required + // +kubebuilder:example="https://git.example.com/my-target-repo.git" + // +kubebuilder:validation:Format=uri + TargetRepository string `json:"targetRepository" protobuf:"bytes,3,name=targetRepository"` + + // +kubebuilder:validation:Required + // +kubebuilder:example:"main" + TargetBranch string `json:"targetBranch" protobuf:"bytes,4,name=targetBranch"` + + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Enum=TryFastForwardOrDie;TryFastForwardOrHardReset;TryHardResetOrDie;"" + MergeStrategy MergeStrategy `json:"mergeStrategy" protobuf:"bytes,5,name=mergeStrategy"` +} + +// RemoteTargetStatus defines the observed state of RemoteTarget. +type RemoteTargetStatus struct { + + // +listType=map + // +listMapKey=type + // +patchStrategy=merge + // +patchMergeKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` + + // +optional + LastObservedCommitHash string `json:"lastObservedCommitHash,omitempty" protobuf:"bytes,2,rep,name=lastObservedCommitHash"` + + // +optional + LastConsistencyOperationType string `json:"lastConsistencyOperationType,omitempty" protobuf:"bytes,3,rep,name=lastConsistencyOperationType"` + + // +optional + LastConsistencyOperationTime string `json:"lastConsistencyOperationTime,omitempty" protobuf:"bytes,4,rep,name=lastConsistencyOperationTime"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +//+kubebuilder:resource:path=remotetargets,shortName=rt;rts,categories=syngit + +// RemoteTarget is the Schema for the remotetargets API. +type RemoteTarget struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RemoteTargetSpec `json:"spec,omitempty"` + Status RemoteTargetStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// RemoteTargetList contains a list of RemoteTarget. +type RemoteTargetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []RemoteTarget `json:"items"` +} + +func init() { + SchemeBuilder.Register(&RemoteTarget{}, &RemoteTargetList{}) +} + +/* + SPEC EXTENSION +*/ + +type MergeStrategy string + +const ( + TryFastForwardOrDie MergeStrategy = "TryFastForwardOrDie" + TryFastForwardOrHardReset MergeStrategy = "TryFastForwardOrHardReset" + TryHardResetOrDie MergeStrategy = "TryHardResetOrDie" +) diff --git a/pkg/api/v1beta3/remoteuser_types.go b/pkg/api/v1beta3/remoteuser_types.go index 19eef75..9abf0d7 100644 --- a/pkg/api/v1beta3/remoteuser_types.go +++ b/pkg/api/v1beta3/remoteuser_types.go @@ -42,16 +42,16 @@ type RemoteUserStatus struct { Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` // +optional - ConnexionStatus RemoteUserConnexionStatus `json:"connexionStatus,omitempty"` + ConnexionStatus RemoteUserConnexionStatus `json:"connexionStatus,omitempty" protobuf:"bytes,2,rep,name=connexionStatus"` // +optional - GitUser string `json:"gitUser,omitempty"` + GitUser string `json:"gitUser,omitempty" protobuf:"bytes,3,rep,name=gitUser"` // +optional - LastAuthTime metav1.Time `json:"lastAuthTime,omitempty"` + LastAuthTime metav1.Time `json:"lastAuthTime,omitempty" protobuf:"bytes,4,rep,name=lastAuthTime"` // +optional - SecretBoundStatus SecretBoundStatus `json:"secretBoundStatus,omitempty"` + SecretBoundStatus SecretBoundStatus `json:"secretBoundStatus,omitempty" protobuf:"bytes,5,rep,name=secretBoundStatus"` } //+kubebuilder:object:root=true diff --git a/pkg/api/v1beta3/remoteuserbinding_types.go b/pkg/api/v1beta3/remoteuserbinding_types.go index 1820002..ca531ac 100644 --- a/pkg/api/v1beta3/remoteuserbinding_types.go +++ b/pkg/api/v1beta3/remoteuserbinding_types.go @@ -24,7 +24,7 @@ import ( const ( RubPrefix = "associated-rub-" - RubAnnotation = "syngit.io/associated-remoteuserbinding" + RubAnnotation = "syngit.io/remoteuserbinding.managed" RemoteRefsField = "spec.remoteRefs" ) @@ -48,16 +48,16 @@ type RemoteUserBindingStatus struct { Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` // +optional - State GitUserBindingState `json:"state,omitempty"` + State GitUserBindingState `json:"state,omitempty" protobuf:"bytes,2,rep,name=state"` // +optional - GitUserHosts []GitUserHost `json:"gitUserHosts"` + GitUserHosts []GitUserHost `json:"gitUserHosts" protobuf:"bytes,3,rep,name=gitUserHosts"` // +optional - UserKubernetesID string `json:"userKubernetesID,omitempty"` + UserKubernetesID string `json:"userKubernetesID,omitempty" protobuf:"bytes,4,rep,name=userKubernetesID"` // +optional - LastUsedTime metav1.Time `json:"lastUsedTime,omitempty"` + LastUsedTime metav1.Time `json:"lastUsedTime,omitempty" protobuf:"bytes,5,rep,name=lastUsedTime"` } //+kubebuilder:object:root=true diff --git a/pkg/api/v1beta3/zz_generated.deepcopy.go b/pkg/api/v1beta3/zz_generated.deepcopy.go index 8a71076..d439351 100644 --- a/pkg/api/v1beta3/zz_generated.deepcopy.go +++ b/pkg/api/v1beta3/zz_generated.deepcopy.go @@ -22,9 +22,9 @@ package v1beta3 import ( admissionregistrationv1 "k8s.io/api/admissionregistration/v1" - "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -140,6 +140,16 @@ func (in *LastObservedObjectState) DeepCopy() *LastObservedObjectState { func (in *LastPushedObjectState) DeepCopyInto(out *LastPushedObjectState) { *out = *in in.LastPushedObjectTime.DeepCopyInto(&out.LastPushedObjectTime) + if in.LastPushedObjectGitRepos != nil { + in, out := &in.LastPushedObjectGitRepos, &out.LastPushedObjectGitRepos + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.LastPushedObjectGitCommitHashes != nil { + in, out := &in.LastPushedObjectGitCommitHashes, &out.LastPushedObjectGitCommitHashes + *out = make([]string, len(*in)) + copy(*out, *in) + } out.LastPushedObject = in.LastPushedObject } @@ -286,6 +296,11 @@ func (in *RemoteSyncerList) DeepCopyObject() runtime.Object { func (in *RemoteSyncerSpec) DeepCopyInto(out *RemoteSyncerSpec) { *out = *in in.ScopedResources.DeepCopyInto(&out.ScopedResources) + if in.RemoteTargetSelector != nil { + in, out := &in.RemoteTargetSelector, &out.RemoteTargetSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } if in.ExcludedFields != nil { in, out := &in.ExcludedFields, &out.ExcludedFields *out = make([]string, len(*in)) @@ -293,12 +308,12 @@ func (in *RemoteSyncerSpec) DeepCopyInto(out *RemoteSyncerSpec) { } if in.ExcludedFieldsConfigMapRef != nil { in, out := &in.ExcludedFieldsConfigMapRef, &out.ExcludedFieldsConfigMapRef - *out = new(v1.ObjectReference) + *out = new(corev1.ObjectReference) **out = **in } if in.RemoteUserBindingSelector != nil { in, out := &in.RemoteUserBindingSelector, &out.RemoteUserBindingSelector - *out = new(metav1.LabelSelector) + *out = new(v1.LabelSelector) (*in).DeepCopyInto(*out) } if in.BypassInterceptionSubjects != nil { @@ -308,7 +323,12 @@ func (in *RemoteSyncerSpec) DeepCopyInto(out *RemoteSyncerSpec) { } if in.DefaultRemoteUserRef != nil { in, out := &in.DefaultRemoteUserRef, &out.DefaultRemoteUserRef - *out = new(v1.ObjectReference) + *out = new(corev1.ObjectReference) + **out = **in + } + if in.DefaultRemoteTargetRef != nil { + in, out := &in.DefaultRemoteTargetRef, &out.DefaultRemoteTargetRef + *out = new(corev1.ObjectReference) **out = **in } out.CABundleSecretRef = in.CABundleSecretRef @@ -329,7 +349,7 @@ func (in *RemoteSyncerStatus) DeepCopyInto(out *RemoteSyncerStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -349,6 +369,102 @@ func (in *RemoteSyncerStatus) DeepCopy() *RemoteSyncerStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RemoteTarget) DeepCopyInto(out *RemoteTarget) { + *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 RemoteTarget. +func (in *RemoteTarget) DeepCopy() *RemoteTarget { + if in == nil { + return nil + } + out := new(RemoteTarget) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RemoteTarget) 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 *RemoteTargetList) DeepCopyInto(out *RemoteTargetList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]RemoteTarget, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemoteTargetList. +func (in *RemoteTargetList) DeepCopy() *RemoteTargetList { + if in == nil { + return nil + } + out := new(RemoteTargetList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RemoteTargetList) 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 *RemoteTargetSpec) DeepCopyInto(out *RemoteTargetSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemoteTargetSpec. +func (in *RemoteTargetSpec) DeepCopy() *RemoteTargetSpec { + if in == nil { + return nil + } + out := new(RemoteTargetSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RemoteTargetStatus) DeepCopyInto(out *RemoteTargetStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemoteTargetStatus. +func (in *RemoteTargetStatus) DeepCopy() *RemoteTargetStatus { + if in == nil { + return nil + } + out := new(RemoteTargetStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RemoteUser) DeepCopyInto(out *RemoteUser) { *out = *in @@ -441,12 +557,12 @@ func (in *RemoteUserBindingSpec) DeepCopyInto(out *RemoteUserBindingSpec) { out.Subject = in.Subject if in.RemoteUserRefs != nil { in, out := &in.RemoteUserRefs, &out.RemoteUserRefs - *out = make([]v1.ObjectReference, len(*in)) + *out = make([]corev1.ObjectReference, len(*in)) copy(*out, *in) } if in.RemoteTargetRefs != nil { in, out := &in.RemoteTargetRefs, &out.RemoteTargetRefs - *out = make([]v1.ObjectReference, len(*in)) + *out = make([]corev1.ObjectReference, len(*in)) copy(*out, *in) } } @@ -466,7 +582,7 @@ func (in *RemoteUserBindingStatus) DeepCopyInto(out *RemoteUserBindingStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -559,7 +675,7 @@ func (in *RemoteUserStatus) DeepCopyInto(out *RemoteUserStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -588,7 +704,7 @@ func (in *ScopedResources) DeepCopyInto(out *ScopedResources) { } if in.ObjectSelector != nil { in, out := &in.ObjectSelector, &out.ObjectSelector - *out = new(metav1.LabelSelector) + *out = new(v1.LabelSelector) (*in).DeepCopyInto(*out) } if in.Rules != nil { diff --git a/pkg/utils/remotetarget-references.go b/pkg/utils/remotetarget-references.go new file mode 100644 index 0000000..3a07929 --- /dev/null +++ b/pkg/utils/remotetarget-references.go @@ -0,0 +1,39 @@ +package utils + +import ( + "fmt" + "net/url" + "strings" + + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" +) + +func RemoteTargetNameConstructor(upstreamRepo string, upstreamBranch string, targetRepo string, targetBranch string) (string, error) { + + upstreamU, err := url.Parse(upstreamRepo) + if err != nil { + return "", err + } + + targetRepoName := syngit.RtDefaultForkName + if targetRepo != "" { + targetU, err := url.Parse(targetRepo) + if err != nil { + return "", err + } + targetRepoName = strings.ReplaceAll(strings.ReplaceAll(targetU.Path, "/", "-"), ".git", "") + } + + upstreamRepoName := strings.ReplaceAll(strings.ReplaceAll(upstreamU.Path, "/", "-"), ".git", "") + name := fmt.Sprintf("%s%s-%s%s-%s", syngit.RtPrefix, upstreamRepoName, upstreamBranch, targetRepoName, targetBranch) + + return name, nil +} + +func GetBranchesFromAnnotation(in string) []string { + out := strings.Split(strings.ReplaceAll(in, " ", ""), ",") + if len(out) == 1 && out[0] == "" { + return []string{} + } + return out +} diff --git a/test/e2e/build/samples/syngit_v1beta3_remotesyncer.yaml b/test/e2e/build/samples/syngit_v1beta3_remotesyncer.yaml index 3ea0246..1b4bee0 100644 --- a/test/e2e/build/samples/syngit_v1beta3_remotesyncer.yaml +++ b/test/e2e/build/samples/syngit_v1beta3_remotesyncer.yaml @@ -6,7 +6,7 @@ spec: remoteRepository: "https://git-fake-server.com/fake-repo.git" defaultBranch: main strategy: CommitOnly - targetStrategy: SameBranch + targetStrategy: OneTarget defaultUnauthorizedUserMode: Block scopedResources: rules: diff --git a/test/e2e/syngit/02_commitonly_cm_test.go b/test/e2e/syngit/02_commitonly_cm_test.go index 8f76899..76272a8 100755 --- a/test/e2e/syngit/02_commitonly_cm_test.go +++ b/test/e2e/syngit/02_commitonly_cm_test.go @@ -18,6 +18,7 @@ package e2e_syngit import ( "context" + "fmt" "strings" . "github.com/onsi/ginkgo/v2" @@ -34,9 +35,10 @@ var _ = Describe("02 CommitOnly a ConfigMap", func() { ctx := context.TODO() const ( - cmName = "test-cm2" - remoteUserLufffyName = "remoteuser-luffy" - remoteSyncerName = "remotesyncer-test2" + cmName = "test-cm2" + remoteUserLuffyName = "remoteuser-luffy" + remoteSyncerName = "remotesyncer-test2" + branch = "main" ) It("should not create the resource on the cluster", func() { @@ -44,7 +46,7 @@ var _ = Describe("02 CommitOnly a ConfigMap", func() { luffySecretName := string(Luffy) + "-creds" remoteUserLuffy := &syngit.RemoteUser{ ObjectMeta: metav1.ObjectMeta{ - Name: remoteUserLufffyName, + Name: remoteUserLuffyName, Namespace: namespace, Annotations: map[string]string{ syngit.RubAnnotation: "true", @@ -69,14 +71,17 @@ var _ = Describe("02 CommitOnly a ConfigMap", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncerName, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, DefaultBlockAppliedMessage: defaultDeniedMessage, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, Strategy: syngit.CommitOnly, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ @@ -113,6 +118,7 @@ var _ = Describe("02 CommitOnly a ConfigMap", func() { cm, metav1.CreateOptions{}, ) + fmt.Println(err) return err != nil && strings.Contains(err.Error(), defaultDeniedMessage) }, timeout, interval).Should(BeTrue()) diff --git a/test/e2e/syngit/03_commitapply_cm_test.go b/test/e2e/syngit/03_commitapply_cm_test.go index ea77d56..1a2cca4 100755 --- a/test/e2e/syngit/03_commitapply_cm_test.go +++ b/test/e2e/syngit/03_commitapply_cm_test.go @@ -37,6 +37,7 @@ var _ = Describe("03 CommitApply a ConfigMap", func() { remoteSyncerName = "remotesyncer-test3" remoteUserLuffyName = "remoteuser-luffy" cmName = "test-cm3" + branch = "main" ) It("should create the resource on the git repo & cluster and delete the resource", func() { @@ -69,14 +70,17 @@ var _ = Describe("03 CommitApply a ConfigMap", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncerName, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ diff --git a/test/e2e/syngit/04_excludedfields_test.go b/test/e2e/syngit/04_excludedfields_test.go index 80ff008..4832365 100755 --- a/test/e2e/syngit/04_excludedfields_test.go +++ b/test/e2e/syngit/04_excludedfields_test.go @@ -38,6 +38,7 @@ var _ = Describe("04 Create RemoteSyncer with excluded fields", func() { cmName2 = "test-cm4.2" remoteUserLuffyName = "remoteuser-luffy" remoteSyncerName = "remotesyncer-test4" + branch = "main" ) It("should exclude the selected fields from the git repo", func() { @@ -70,11 +71,14 @@ var _ = Describe("04 Create RemoteSyncer with excluded fields", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncerName, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, DefaultBlockAppliedMessage: defaultDeniedMessage, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{ ".metadata.uid", @@ -83,7 +87,7 @@ var _ = Describe("04 Create RemoteSyncer with excluded fields", func() { "metadata.annotations.[test-annotation2]", }, Strategy: syngit.CommitOnly, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ @@ -211,18 +215,21 @@ var _ = Describe("04 Create RemoteSyncer with excluded fields", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncerName, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, DefaultBlockAppliedMessage: defaultDeniedMessage, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFieldsConfigMapRef: &corev1.ObjectReference{ Name: excludedFieldsConfiMapName, Namespace: namespace, }, Strategy: syngit.CommitOnly, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ diff --git a/test/e2e/syngit/05_defaultuser_test.go b/test/e2e/syngit/05_defaultuser_test.go index d245260..66773e2 100644 --- a/test/e2e/syngit/05_defaultuser_test.go +++ b/test/e2e/syngit/05_defaultuser_test.go @@ -34,11 +34,13 @@ var _ = Describe("05 Use a default user", func() { ctx := context.TODO() const ( - cmName = "test-cm5" - remoteUserLuffyName = "remoteuser-luffy" - remoteUserChopperName = "remoteuser-chopper" - remoteUserSanjiName = "remoteuser-sanji" - remoteSyncerName = "remotesyncer-test5" + cmName = "test-cm5" + remoteUserLuffyName = "remoteuser-luffy" + remoteUserChopperName = "remoteuser-chopper" + remoteUserSanjiName = "remoteuser-sanji" + remoteSyncerName = "remotesyncer-test5" + branch = "main" + defaultRemoteTargetName = "remotesyncer-test5" ) It("should use the default user to push the resource", func() { @@ -83,23 +85,46 @@ var _ = Describe("05 Use a default user", func() { }, timeout, interval).Should(BeTrue()) repoUrl := "https://" + gitP1Fqdn + "/syngituser/green.git" + By("creating the default RemoteTarget") + remoteTarget := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: defaultRemoteTargetName, + Namespace: namespace, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repoUrl, + TargetRepository: repoUrl, + UpstreamBranch: branch, + TargetBranch: branch, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTarget) + return err == nil + }, timeout, interval).Should(BeTrue()) + By("creating the RemoteSyncer") remotesyncer := &syngit.RemoteSyncer{ ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncerName, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.UseDefaultUser, ExcludedFields: []string{".metadata.uid"}, DefaultRemoteUserRef: &corev1.ObjectReference{ - Name: remoteUserChopperName, - Namespace: namespace, + Name: remoteUserChopperName, + }, + DefaultRemoteTargetRef: &corev1.ObjectReference{ + Name: defaultRemoteTargetName, }, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ diff --git a/test/e2e/syngit/06_objects_lifecycle_test.go b/test/e2e/syngit/06_objects_lifecycle_test.go index 9fd4c3c..577fd59 100644 --- a/test/e2e/syngit/06_objects_lifecycle_test.go +++ b/test/e2e/syngit/06_objects_lifecycle_test.go @@ -32,10 +32,11 @@ import ( var _ = Describe("06 Test objects lifecycle", func() { const ( - remoteUserLuffyName = "remoteuser-luffy" - remoteUserLuffyJupyterName = "remoteuser-luffy" + remoteUserLuffyName = "remoteuser-luffy-jupyter" + remoteUserLuffyJupyterName = "remoteuser-luffy-jupyter" remoteUserLuffySaturnName = "remoteuser-luffy-saturn" remoteSyncerName = "remotesyncer-test6" + branch = "main" ) It("should properly manage the RemoteUserBinding associated webhooks", func() { @@ -182,14 +183,17 @@ var _ = Describe("06 Test objects lifecycle", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncerName, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ diff --git a/test/e2e/syngit/07_bypass_subject_test.go b/test/e2e/syngit/07_bypass_subject_test.go index 2fe5190..00402db 100644 --- a/test/e2e/syngit/07_bypass_subject_test.go +++ b/test/e2e/syngit/07_bypass_subject_test.go @@ -37,6 +37,7 @@ var _ = Describe("07 Subject bypasses interception", func() { remoteSyncer1Name = "remotesyncer-test7.1" remoteSyncer2Name = "remotesyncer-test7.2" cmName = "test-cm7" + branch = "main" ) It("should apply the resource on the cluster but not push it on the git repository (CommitApply)", func() { //nolint:dupl @@ -72,10 +73,13 @@ var _ = Describe("07 Subject bypasses interception", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncer1Name, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, BypassInterceptionSubjects: []v1.Subject{{ APIGroup: "rbac.authorization.k8s.io", @@ -84,7 +88,7 @@ var _ = Describe("07 Subject bypasses interception", func() { }}, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ @@ -182,10 +186,13 @@ var _ = Describe("07 Subject bypasses interception", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncer2Name, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, BypassInterceptionSubjects: []v1.Subject{{ APIGroup: "rbac.authorization.k8s.io", @@ -194,7 +201,7 @@ var _ = Describe("07 Subject bypasses interception", func() { }}, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitOnly, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ diff --git a/test/e2e/syngit/08_webhook_rbac_test.go b/test/e2e/syngit/08_webhook_rbac_test.go index 87a672f..c5924c5 100644 --- a/test/e2e/syngit/08_webhook_rbac_test.go +++ b/test/e2e/syngit/08_webhook_rbac_test.go @@ -18,6 +18,7 @@ package e2e_syngit import ( "context" + "fmt" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -38,6 +39,7 @@ var _ = Describe("08 Webhook rbac checker", func() { remoteSyncer2Name = "remotesyncer-test8.2" cmName = "test-cm8" secretName = "test-secret8" + branch = "main" ) ctx := context.TODO() @@ -72,14 +74,17 @@ var _ = Describe("08 Webhook rbac checker", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncer1Name, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ @@ -175,14 +180,17 @@ var _ = Describe("08 Webhook rbac checker", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncer2Name, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ @@ -210,14 +218,17 @@ var _ = Describe("08 Webhook rbac checker", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncer2Name, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ @@ -254,6 +265,7 @@ var _ = Describe("08 Webhook rbac checker", func() { secret, metav1.CreateOptions{}, ) + fmt.Println(err) return err == nil }, timeout, interval).Should(BeTrue()) diff --git a/test/e2e/syngit/09_multi_remotesyncer_test.go b/test/e2e/syngit/09_multi_remotesyncer_test.go index 8fa915e..bf8531d 100644 --- a/test/e2e/syngit/09_multi_remotesyncer_test.go +++ b/test/e2e/syngit/09_multi_remotesyncer_test.go @@ -38,6 +38,7 @@ var _ = Describe("09 Multi RemoteSyncer test", func() { remoteSyncer2Name = "remotesyncer-test9.2" cmName1 = "test-cm9.1" cmName2 = "test-cm9.2" + branch = "main" ) ctx := context.TODO() @@ -72,14 +73,17 @@ var _ = Describe("09 Multi RemoteSyncer test", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncer1Name, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: blueUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ @@ -105,14 +109,17 @@ var _ = Describe("09 Multi RemoteSyncer test", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncer2Name, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: greenUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ @@ -216,14 +223,17 @@ var _ = Describe("09 Multi RemoteSyncer test", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncer1Name, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: blueUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ @@ -248,14 +258,17 @@ var _ = Describe("09 Multi RemoteSyncer test", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncer2Name, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: blueUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ diff --git a/test/e2e/syngit/11_remoteuserbinding_permissions_test.go b/test/e2e/syngit/11_remoteuserbinding_permissions_test.go index b8ea501..5a84732 100644 --- a/test/e2e/syngit/11_remoteuserbinding_permissions_test.go +++ b/test/e2e/syngit/11_remoteuserbinding_permissions_test.go @@ -65,8 +65,7 @@ var _ = Describe("11 RemoteUserBinding permissions checker", func() { Spec: syngit.RemoteUserBindingSpec{ RemoteUserRefs: []corev1.ObjectReference{ { - Name: "not-allowed-remoteuser-name", - Namespace: namespace, + Name: "not-allowed-remoteuser-name", }, }, }, @@ -109,8 +108,7 @@ var _ = Describe("11 RemoteUserBinding permissions checker", func() { Spec: syngit.RemoteUserBindingSpec{ RemoteUserRefs: []corev1.ObjectReference{ { - Name: remoteUserBrookName, - Namespace: namespace, + Name: remoteUserBrookName, }, }, }, diff --git a/test/e2e/syngit/12_remoteuserbinding_managed_test.go b/test/e2e/syngit/12_remoteuserbinding_managed_test.go index fe6c212..dd92d91 100644 --- a/test/e2e/syngit/12_remoteuserbinding_managed_test.go +++ b/test/e2e/syngit/12_remoteuserbinding_managed_test.go @@ -66,8 +66,7 @@ var _ = Describe("12 RemoteUserBinding managed by checker", func() { Spec: syngit.RemoteUserBindingSpec{ RemoteUserRefs: []corev1.ObjectReference{ { - Name: remoteUserLuffyName, - Namespace: namespace, + Name: remoteUserLuffyName, }, }, }, @@ -87,8 +86,7 @@ var _ = Describe("12 RemoteUserBinding managed by checker", func() { Spec: syngit.RemoteUserBindingSpec{ RemoteUserRefs: []corev1.ObjectReference{ { - Name: remoteUserLuffyName, - Namespace: namespace, + Name: remoteUserLuffyName, }, }, }, diff --git a/test/e2e/syngit/13_remotesyncer_tls_test.go b/test/e2e/syngit/13_remotesyncer_tls_test.go index 240c4e3..d5fb426 100644 --- a/test/e2e/syngit/13_remotesyncer_tls_test.go +++ b/test/e2e/syngit/13_remotesyncer_tls_test.go @@ -48,6 +48,7 @@ var _ = Describe("13 RemoteSyncer TLS insecure & custom CA bundle test", func() remoteSyncerName5 = "remotesyncer-test13.5" secretCa1 = "custom-cabundle1" secretCa2 = "custom-cabundle2" + branch = "main" ) It("should interact with gitea using the user CA bundle", func() { @@ -101,16 +102,19 @@ var _ = Describe("13 RemoteSyncer TLS insecure & custom CA bundle test", func() ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncerName2, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ CABundleSecretRef: corev1.SecretReference{ Name: secretCa1, }, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ @@ -226,17 +230,20 @@ var _ = Describe("13 RemoteSyncer TLS insecure & custom CA bundle test", func() ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncerName3, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ CABundleSecretRef: corev1.SecretReference{ Name: secretCa2, Namespace: namespace, }, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ @@ -353,13 +360,16 @@ var _ = Describe("13 RemoteSyncer TLS insecure & custom CA bundle test", func() ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncerName4, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ @@ -462,13 +472,16 @@ var _ = Describe("13 RemoteSyncer TLS insecure & custom CA bundle test", func() ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncerName5, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ diff --git a/test/e2e/syngit/15_conversion_webhook_test.go b/test/e2e/syngit/15_conversion_webhook_test.go index 46d7546..e2f6966 100644 --- a/test/e2e/syngit/15_conversion_webhook_test.go +++ b/test/e2e/syngit/15_conversion_webhook_test.go @@ -17,8 +17,6 @@ limitations under the License. package e2e_syngit import ( - "fmt" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" syngitv1beta2 "github.com/syngit-org/syngit/pkg/api/v1beta2" @@ -56,7 +54,6 @@ var _ = Describe("15 conversion webhook test", func() { } Eventually(func() bool { err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) - fmt.Println(err) return err == nil }, timeout, interval).Should(BeTrue()) @@ -80,8 +77,7 @@ var _ = Describe("15 conversion webhook test", func() { Spec: syngitv1beta2.RemoteUserBindingSpec{ RemoteRefs: []corev1.ObjectReference{ { - Name: "fake-remoteuser", - Namespace: namespace, + Name: "fake-remoteuser", }, }, }, @@ -134,7 +130,6 @@ var _ = Describe("15 conversion webhook test", func() { } Eventually(func() bool { err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) - fmt.Println(err) return err == nil }, timeout, interval).Should(BeTrue()) diff --git a/test/e2e/syngit/16_wrong_reference_value_test.go b/test/e2e/syngit/16_wrong_reference_value_test.go index 9b7dd9c..e8bd32b 100644 --- a/test/e2e/syngit/16_wrong_reference_value_test.go +++ b/test/e2e/syngit/16_wrong_reference_value_test.go @@ -36,8 +36,10 @@ var _ = Describe("16 Wrong reference or value test", func() { const ( remoteSyncerName = "remotesyncer-test16" remoteUserLuffyName = "remoteuser-luffy" + remoteUserChopperName = "remoteuser-chopper" remoteUserBindingLuffyName = "remoteuserbinding-luffy" cmName = "test-cm16" + branch = "main" ) It("should get errored because of wrong resource reference or wrong value", func() { @@ -81,8 +83,12 @@ var _ = Describe("16 Wrong reference or value test", func() { Spec: syngit.RemoteUserBindingSpec{ RemoteUserRefs: []corev1.ObjectReference{ { - Name: "fake-remoteuser", - Namespace: namespace, + Name: "fake-remoteuser", + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: "fake-remotetarget", }, }, }, @@ -98,14 +104,17 @@ var _ = Describe("16 Wrong reference or value test", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncerName, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ @@ -172,17 +181,23 @@ var _ = Describe("16 Wrong reference or value test", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncerName, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.UseDefaultUser, DefaultRemoteUserRef: &corev1.ObjectReference{ Name: "fake-defaultuser", }, + DefaultRemoteTargetRef: &corev1.ObjectReference{ + Name: "fake-defaulttarget", + }, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ @@ -227,5 +242,91 @@ var _ = Describe("16 Wrong reference or value test", func() { return err != nil && strings.Contains(err.Error(), notPresentOnCluser) }, timeout, interval).Should(BeTrue()) + By("only creating the RemoteUser for Chopper") + chopperSecretName := string(Chopper) + "-creds" + remoteUserChopper := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserChopperName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: chopperSecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Chopper).CreateOrUpdate(remoteUserChopper) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer using wrong referenced default target") + remotesyncer = &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: branch, + DefaultUnauthorizedUserMode: syngit.UseDefaultUser, + DefaultRemoteUserRef: &corev1.ObjectReference{ + Name: remoteUserChopperName, + }, + DefaultRemoteTargetRef: &corev1.ObjectReference{ + Name: "fake-defaulttarget", + }, + ExcludedFields: []string{".metadata.uid"}, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap") + Wait3() + Eventually(func() bool { + _, err = sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm, + metav1.CreateOptions{}, + ) + return err != nil && strings.Contains(err.Error(), defaultTargetNotFound) + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is not present on the repo") + Wait3() + exists, err = IsObjectInRepo(*repo, cm) + Expect(err).To(HaveOccurred()) + Expect(exists).To(BeFalse()) + + By("checking that the configmap is not present on the cluster") + getCm = &corev1.ConfigMap{} + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err != nil && strings.Contains(err.Error(), notPresentOnCluser) + }, timeout, interval).Should(BeTrue()) + }) }) diff --git a/test/e2e/syngit/17_remoteuserbinding_selector_test.go b/test/e2e/syngit/17_remoteuserbinding_selector_test.go index 8d5ed00..7d0e3f4 100644 --- a/test/e2e/syngit/17_remoteuserbinding_selector_test.go +++ b/test/e2e/syngit/17_remoteuserbinding_selector_test.go @@ -18,6 +18,7 @@ package e2e_syngit import ( "context" + "fmt" "strings" . "github.com/onsi/ginkgo/v2" @@ -44,9 +45,12 @@ var _ = Describe("17 RemoteUserBinding selector in RemoteSyncer", func() { cmName1 = "test-cm17.1" cmName2 = "test-cm17.2" cmName3 = "test-cm17.3" + branch = "main" ) - It("should not work because RemoteUserBinding not targeted", func() { + remoteTargetName := fmt.Sprintf("%s-syngituser-blue-%s-syngituser-blue-%s", syngit.RtPrefix, branch, branch) + + It("should not push because RemoteUserBinding not targeted", func() { By("creating the RemoteUser & RemoteUserBinding for Luffy") luffySecretName := string(Luffy) + "-creds" remoteUserLuffy := &syngit.RemoteUser{ @@ -82,8 +86,12 @@ var _ = Describe("17 RemoteUserBinding selector in RemoteSyncer", func() { Spec: syngit.RemoteUserBindingSpec{ RemoteUserRefs: []corev1.ObjectReference{ { - Name: remoteUserLuffyName, - Namespace: namespace, + Name: remoteUserLuffyName, + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: remoteTargetName, }, }, Subject: rbacv1.Subject{ @@ -103,14 +111,17 @@ var _ = Describe("17 RemoteUserBinding selector in RemoteSyncer", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncerName1, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, RemoteUserBindingSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{myLabelKey: "another-value"}, @@ -178,8 +189,8 @@ var _ = Describe("17 RemoteUserBinding selector in RemoteSyncer", func() { }) - It("should work because RemoteUserBinding is targeted", func() { - By("creating the RemoteUser & RemoteUserBinding for Luffy") + It("should push because RemoteUserBinding is targeted", func() { + By("creating the RemoteUser") luffySecretName := string(Luffy) + "-creds" remoteUserLuffy := &syngit.RemoteUser{ ObjectMeta: metav1.ObjectMeta{ @@ -214,8 +225,12 @@ var _ = Describe("17 RemoteUserBinding selector in RemoteSyncer", func() { Spec: syngit.RemoteUserBindingSpec{ RemoteUserRefs: []corev1.ObjectReference{ { - Name: remoteUserLuffyName, - Namespace: namespace, + Name: remoteUserLuffyName, + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: remoteTargetName, }, }, Subject: rbacv1.Subject{ @@ -235,14 +250,17 @@ var _ = Describe("17 RemoteUserBinding selector in RemoteSyncer", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncerName2, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, RemoteUserBindingSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{myLabelKey: myLabelValue}, @@ -316,7 +334,7 @@ var _ = Describe("17 RemoteUserBinding selector in RemoteSyncer", func() { err := syngit.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) - By("creating the RemoteUser & RemoteUserBinding for Luffy") + By("creating the RemoteUser") luffySecretName := string(Luffy) + "-creds" remoteUserLuffy := &syngit.RemoteUser{ ObjectMeta: metav1.ObjectMeta{ @@ -351,8 +369,12 @@ var _ = Describe("17 RemoteUserBinding selector in RemoteSyncer", func() { Spec: syngit.RemoteUserBindingSpec{ RemoteUserRefs: []corev1.ObjectReference{ { - Name: remoteUserLuffyName, - Namespace: namespace, + Name: remoteUserLuffyName, + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: remoteTargetName, }, }, Subject: rbacv1.Subject{ @@ -372,14 +394,17 @@ var _ = Describe("17 RemoteUserBinding selector in RemoteSyncer", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncerName3, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, ExcludedFields: []string{".metadata.uid"}, Strategy: syngit.CommitApply, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ diff --git a/test/e2e/syngit/18_cluster_default_excludedfields_test.go b/test/e2e/syngit/18_cluster_default_excludedfields_test.go index 4f97292..4403143 100644 --- a/test/e2e/syngit/18_cluster_default_excludedfields_test.go +++ b/test/e2e/syngit/18_cluster_default_excludedfields_test.go @@ -37,6 +37,7 @@ var _ = Describe("18 Cluster default excluded fields test", func() { cmName1 = "test-cm18" remoteUserLuffyName = "remoteuser-luffy" remoteSyncerName = "remotesyncer-test18" + branch = "main" ) It("should exclude the cluster default fields from the git repo", func() { @@ -93,14 +94,17 @@ var _ = Describe("18 Cluster default excluded fields test", func() { ObjectMeta: metav1.ObjectMeta{ Name: remoteSyncerName, Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, }, Spec: syngit.RemoteSyncerSpec{ InsecureSkipTlsVerify: true, DefaultBlockAppliedMessage: defaultDeniedMessage, - DefaultBranch: "main", + DefaultBranch: branch, DefaultUnauthorizedUserMode: syngit.Block, Strategy: syngit.CommitOnly, - TargetStrategy: syngit.SameBranch, + TargetStrategy: syngit.OneTarget, RemoteRepository: repoUrl, ScopedResources: syngit.ScopedResources{ Rules: []admissionv1.RuleWithOperations{{ diff --git a/test/e2e/syngit/19_remotetarget_same_repo_branch_test.go b/test/e2e/syngit/19_remotetarget_same_repo_branch_test.go new file mode 100644 index 0000000..f77925b --- /dev/null +++ b/test/e2e/syngit/19_remotetarget_same_repo_branch_test.go @@ -0,0 +1,58 @@ +/* +Copyright 2024. + +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 e2e_syngit + +import ( + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + . "github.com/syngit-org/syngit/test/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("19 RemoteTarget same repo & branch between target and upstream test", func() { + + const ( + remoteTargetName = "remotetarget-test19" + repo = "https://my-server.com/my-upstream-repo.git" + branch = "main" + ) + + It("should deny the RemoteTarget creation", func() { + By("creating a RemoteTarget with the same repo & branch for the target & upstream and strategy setup") + remoteTarget := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteTargetName, + Namespace: namespace, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repo, + TargetRepository: repo, + UpstreamBranch: branch, + TargetBranch: branch, + MergeStrategy: syngit.TryFastForwardOrHardReset, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTarget) + return err != nil && strings.Contains(err.Error(), sameBranchRepo) + }, timeout, interval).Should(BeTrue()) + + }) +}) diff --git a/test/e2e/syngit/20_objects_without_annotations_test.go b/test/e2e/syngit/20_objects_without_annotations_test.go new file mode 100644 index 0000000..196d7e4 --- /dev/null +++ b/test/e2e/syngit/20_objects_without_annotations_test.go @@ -0,0 +1,190 @@ +/* +Copyright 2024. + +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 e2e_syngit + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + . "github.com/syngit-org/syngit/test/utils" + admissionv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("20 All syngit objects without annotations test", func() { + ctx := context.TODO() + + const ( + remoteUserLuffyName = "remoteuser-luffy" + remoteUserBindingLuffyName = "remoteuserbinding-luffy" + remoteTargetName = "remotetarget-test20" + remoteSyncerName = "remotesyncer-test20" + cmName = "test-cm20" + branch = "main" + ) + + It("should process the all workflow by creating the ConfigMap", func() { + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + + By("creating the RemoteUser for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a RemoteTarget with the same repo & branch for the target & upstream") + remoteTarget := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteTargetName, + Namespace: namespace, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repoUrl, + TargetRepository: repoUrl, + UpstreamBranch: branch, + TargetBranch: branch, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTarget) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteUserBinding for Luffy") + remoteUserBindingLuffy := &syngit.RemoteUserBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserBindingLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserBindingSpec{ + RemoteUserRefs: []corev1.ObjectReference{ + { + Name: remoteUserLuffyName, + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: remoteTargetName, + }, + }, + Subject: v1.Subject{ + Kind: "User", + Name: string(Luffy), + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserBindingLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer") + remotesyncer := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName, + Namespace: namespace, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: branch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap") + Wait3() + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the repo") + Wait3() + repo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + } + exists, err := IsObjectInRepo(*repo, cm) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm := types.NamespacedName{ + Name: cmName, + Namespace: namespace, + } + getCm := &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + }) +}) diff --git a/test/e2e/syngit/21_remotetarget_one_different_branch_test.go b/test/e2e/syngit/21_remotetarget_one_different_branch_test.go new file mode 100644 index 0000000..3732537 --- /dev/null +++ b/test/e2e/syngit/21_remotetarget_one_different_branch_test.go @@ -0,0 +1,259 @@ +/* +Copyright 2024. + +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 e2e_syngit + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + . "github.com/syngit-org/syngit/test/utils" + admissionv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("21 RemoteTarget one different branch", func() { + ctx := context.TODO() + + const ( + remoteUserLuffyName = "remoteuser-luffy" + remoteSyncerName1 = "remotesyncer-test21.1" + remoteSyncerName2 = "remotesyncer-test21.2" + cmName1 = "test-cm21.1" + cmName2 = "test-cm21.2" + upstreamBranch = "main" + customBranch = "custom-branch21" + ) + + It("should push the ConfigMap to the Luffy branch (using one strategy)", func() { + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + targetBranch := string(Luffy) + + By("creating the RemoteUser & RemoteUserBinding for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserLuffyName, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RubAnnotation: "true", + }, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer") + remotesyncer := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName1, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationUserSpecificKey: string(syngit.RtAnnotationOneUserOneBranchValue), + }, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: upstreamBranch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap") + Wait3() + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName1, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the repo") + Wait3() + repo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: targetBranch, + } + exists, err := IsObjectInRepo(*repo, cm) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm := types.NamespacedName{ + Name: cmName1, + Namespace: namespace, + } + getCm := &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + }) + + It("should push the ConfigMap to the second-branch branch (using multiple strategy)", func() { + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + + By("creating the RemoteUser for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserLuffyName, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RubAnnotation: "true", + }, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer") + remotesyncer := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName2, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: customBranch, + }, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: upstreamBranch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.MultipleTarget, + RemoteRepository: repoUrl, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap") + Wait3() + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName2, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the repo") + Wait3() + repo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: customBranch, + } + exists, err := IsObjectInRepo(*repo, cm) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm := types.NamespacedName{ + Name: cmName2, + Namespace: namespace, + } + getCm := &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + }) +}) diff --git a/test/e2e/syngit/22_remotetarget_multiple_different_branch_test.go b/test/e2e/syngit/22_remotetarget_multiple_different_branch_test.go new file mode 100644 index 0000000..48b0b34 --- /dev/null +++ b/test/e2e/syngit/22_remotetarget_multiple_different_branch_test.go @@ -0,0 +1,288 @@ +/* +Copyright 2024. + +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 e2e_syngit + +import ( + "context" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + . "github.com/syngit-org/syngit/test/utils" + admissionv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("22 RemoteTarget multiple different branch", func() { + ctx := context.TODO() + + const ( + remoteUserLuffyName = "remoteuser-luffy" + remoteSyncerName1 = "remotesyncer-test22.1" + remoteSyncerName2 = "remotesyncer-test22.2" + cmName1 = "test-cm22.1" + cmName2 = "test-cm22.2" + upstreamBranch = "main" + branch1 = "different-branch1" + branch2 = "different-branch2" + branch3 = "different-branch3" + ) + + It("should push the ConfigMap to all the branches and the Luffy's branch", func() { + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + targetBranch := string(Luffy) + + By("creating the RemoteUser for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserLuffyName, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RubAnnotation: "true", + }, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer") + branches := []string{branch1, branch2, branch3} + remotesyncer := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName1, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationUserSpecificKey: string(syngit.RtAnnotationOneUserOneBranchValue), + syngit.RtAnnotationOneOrManyBranchesKey: strings.Join(branches, ","), + }, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: upstreamBranch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.MultipleTarget, + RemoteRepository: repoUrl, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap") + Wait3() + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName1, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present in all the branches") + Wait3() + repo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: targetBranch, + } + exists, err := IsObjectInRepo(*repo, cm) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + for _, branch := range branches { + repo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: branch, + } + exists, err := IsObjectInRepo(*repo, cm) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + } + + By("checking that the configmap is present on the cluster") + nnCm := types.NamespacedName{ + Name: cmName1, + Namespace: namespace, + } + getCm := &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + }) + + It("should deny the request because of the wrong strategy", func() { + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + targetBranch := string(Luffy) + + By("creating the RemoteUser for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserLuffyName, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RubAnnotation: "true", + }, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer") + branches := []string{branch1, branch2, branch3} + remotesyncer := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName2, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationUserSpecificKey: string(syngit.RtAnnotationOneUserOneBranchValue), + syngit.RtAnnotationOneOrManyBranchesKey: strings.Join(branches, ","), + }, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: upstreamBranch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap") + Wait3() + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName2, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm, + metav1.CreateOptions{}, + ) + return err != nil && strings.Contains(err.Error(), oneTargetForMultipleMessage) + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is not present on the branches") + Wait3() + repo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: targetBranch, + } + exists, err := IsObjectInRepo(*repo, cm) + Expect(err).To(HaveOccurred()) + Expect(exists).To(BeFalse()) + for _, branch := range branches { + repo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: branch, + } + exists, err := IsObjectInRepo(*repo, cm) + Expect(err).To(HaveOccurred()) + Expect(exists).To(BeFalse()) + } + + By("checking that the configmap is not present on the cluster") + nnCm := types.NamespacedName{ + Name: cmName1, + Namespace: namespace, + } + getCm := &corev1.ConfigMap{} + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err != nil && strings.Contains(err.Error(), notPresentOnCluser) + }, timeout, interval).Should(BeTrue()) + + }) +}) diff --git a/test/e2e/syngit/23_remotetarger_selector._test.go b/test/e2e/syngit/23_remotetarger_selector._test.go new file mode 100644 index 0000000..dd1e071 --- /dev/null +++ b/test/e2e/syngit/23_remotetarger_selector._test.go @@ -0,0 +1,848 @@ +/* +Copyright 2024. + +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 e2e_syngit + +import ( + "context" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + . "github.com/syngit-org/syngit/test/utils" + admissionv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" +) + +var _ = Describe("23 RemoteTarget selector in RemoteSyncer", func() { + ctx := context.TODO() + + const ( + remoteSyncerName1 = "remotesyncer-test23.1" + remoteSyncerName2 = "remotesyncer-test23.2" + remoteSyncerName3 = "remotesyncer-test23.3" + remoteSyncerName4 = "remotesyncer-test23.4" + remoteSyncerName5 = "remotesyncer-test23.5" + remoteTargetName1 = "remotetarget-test23.1" + remoteTargetName2 = "remotetarget-test23.2" + remoteTargetName3 = "remotetarget-test23.3" + remoteTargetName41 = "remotetarget-test23.41" + remoteTargetName42 = "remotetarget-test23.42" + remoteTargetName5 = "remotetarget-test23.5" + remoteUserLuffyName = "remoteuser-luffy" + remoteUserBindingLuffyName = "remoteuserbinding-luffy" + cmName1 = "test-cm23.1" + cmName2 = "test-cm23.2" + cmName3 = "test-cm23.3" + cmName4 = "test-cm23.4" + cmName5 = "test-cm23.5" + branch = "main" + ) + + It("should not work because RemoteTarget not targeted", func() { + By("creating the RemoteUser for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + By("creating a RemoteTarget") + const ( + myLabelKey = "my-label-key" + myLabelValue = "my-label-value" + ) + remoteTarget := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteTargetName1, + Namespace: namespace, + Labels: map[string]string{myLabelKey: myLabelValue}, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repoUrl, + TargetRepository: repoUrl, + UpstreamBranch: branch, + TargetBranch: branch, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTarget) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteUserBinding with the RemoteUser & RemoteTarget") + remoteUserBindingLuffy := &syngit.RemoteUserBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserBindingLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserBindingSpec{ + RemoteUserRefs: []corev1.ObjectReference{ + { + Name: remoteUserLuffyName, + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: remoteTargetName1, + }, + }, + Subject: v1.Subject{ + Kind: "User", + Name: string(Luffy), + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserBindingLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer") + remotesyncer := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName1, + Namespace: namespace, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: branch, + DefaultUnauthorizedUserMode: syngit.Block, + ExcludedFields: []string{".metadata.uid"}, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + RemoteTargetSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{myLabelKey: "another-value"}, + }, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + admissionv1.Delete, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap") + Wait3() + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName1, Namespace: namespace}, + Data: map[string]string{"test": "ouiiii"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm, + metav1.CreateOptions{}, + ) + return err != nil && strings.Contains(err.Error(), rtNotFound) + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is not present on the repo") + Wait3() + repo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: branch, + } + exists, err := IsObjectInRepo(*repo, cm) + Expect(err).To(HaveOccurred()) + Expect(exists).To(BeFalse()) + + By("checking that the configmap is not present on the cluster") + nnCm := types.NamespacedName{ + Name: cmName1, + Namespace: namespace, + } + getCm := &corev1.ConfigMap{} + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err != nil && strings.Contains(err.Error(), notPresentOnCluser) + }, timeout, interval).Should(BeTrue()) + + }) + + It("should work because RemoteTarget is targeted", func() { + By("creating the RemoteUser for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + By("creating a RemoteTarget") + const ( + myLabelKey = "my-label-key" + myLabelValue = "my-label-value" + ) + remoteTarget := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteTargetName2, + Namespace: namespace, + Labels: map[string]string{myLabelKey: myLabelValue}, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repoUrl, + TargetRepository: repoUrl, + UpstreamBranch: branch, + TargetBranch: branch, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTarget) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteUserBinding with the RemoteUser & RemoteTarget") + remoteUserBindingLuffy := &syngit.RemoteUserBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserBindingLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserBindingSpec{ + RemoteUserRefs: []corev1.ObjectReference{ + { + Name: remoteUserLuffyName, + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: remoteTargetName2, + }, + }, + Subject: v1.Subject{ + Kind: "User", + Name: string(Luffy), + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserBindingLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer") + remotesyncer2 := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName2, + Namespace: namespace, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: branch, + DefaultUnauthorizedUserMode: syngit.Block, + ExcludedFields: []string{".metadata.uid"}, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + RemoteTargetSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{myLabelKey: myLabelValue}, + }, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + admissionv1.Delete, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer2) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap") + Wait3() + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName2, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the repo") + Wait3() + repo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + } + exists, err := IsObjectInRepo(*repo, cm) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm := types.NamespacedName{ + Name: cmName2, + Namespace: namespace, + } + getCm := &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + }) + + It("should work because RemoteTarget selector not specified", func() { + By("adding syngit to scheme") + err := syngit.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + By("creating the RemoteUser for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + By("creating a RemoteTarget") + const ( + myLabelKey = "my-label-key" + myLabelValue = "my-label-value" + ) + remoteTarget := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteTargetName3, + Namespace: namespace, + Labels: map[string]string{myLabelKey: myLabelValue}, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repoUrl, + TargetRepository: repoUrl, + UpstreamBranch: branch, + TargetBranch: branch, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTarget) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteUserBinding with the RemoteUser & RemoteTarget") + remoteUserBindingLuffy := &syngit.RemoteUserBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserBindingLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserBindingSpec{ + RemoteUserRefs: []corev1.ObjectReference{ + { + Name: remoteUserLuffyName, + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: remoteTargetName3, + }, + }, + Subject: v1.Subject{ + Kind: "User", + Name: string(Luffy), + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserBindingLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer") + remotesyncer3 := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName3, + Namespace: namespace, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: branch, + DefaultUnauthorizedUserMode: syngit.Block, + ExcludedFields: []string{".metadata.uid"}, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + admissionv1.Delete, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer3) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap") + Wait3() + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName3, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err = sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking if the configmap is present on the repo") + Wait3() + repo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + } + exists, err := IsObjectInRepo(*repo, cm) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm := types.NamespacedName{ + Name: cmName3, + Namespace: namespace, + } + getCm := &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + }) + + It("should work with multiple RemoteTarget", func() { + By("adding syngit to scheme") + err := syngit.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + By("creating the RemoteUser for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + By("creating the first RemoteTarget") + const ( + myLabelKey = "my-label-key" + myLabelValue = "my-label-value" + branch1 = "branch23-31" + branch2 = "branch23-32" + ) + remoteTarget41 := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteTargetName41, + Namespace: namespace, + Labels: map[string]string{myLabelKey: myLabelValue}, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repoUrl, + TargetRepository: repoUrl, + UpstreamBranch: branch, + TargetBranch: branch1, + MergeStrategy: syngit.TryFastForwardOrDie, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTarget41) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the second RemoteTarget") + remoteTarget42 := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteTargetName42, + Namespace: namespace, + Labels: map[string]string{myLabelKey: myLabelValue}, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repoUrl, + TargetRepository: repoUrl, + UpstreamBranch: branch, + TargetBranch: branch2, + MergeStrategy: syngit.TryFastForwardOrDie, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTarget42) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteUserBinding with the RemoteUser & RemoteTarget") + remoteUserBindingLuffy := &syngit.RemoteUserBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserBindingLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserBindingSpec{ + RemoteUserRefs: []corev1.ObjectReference{ + { + Name: remoteUserLuffyName, + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: remoteTargetName41, + }, + { + Name: remoteTargetName42, + }, + }, + Subject: v1.Subject{ + Kind: "User", + Name: string(Luffy), + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserBindingLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer") + remotesyncer4 := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName4, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: branch, + DefaultUnauthorizedUserMode: syngit.Block, + ExcludedFields: []string{".metadata.uid"}, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.MultipleTarget, + RemoteTargetSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{myLabelKey: myLabelValue}, + }, + RemoteRepository: repoUrl, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + admissionv1.Delete, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer4) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap") + Wait3() + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName4, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err = sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking if the configmap is present on the branches") + Wait3() + repo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: branch1, + } + exists, err := IsObjectInRepo(*repo, cm) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + repo = &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: branch2, + } + exists, err = IsObjectInRepo(*repo, cm) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm := types.NamespacedName{ + Name: cmName4, + Namespace: namespace, + } + getCm := &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + }) + + It("should not work because RemoteTarget is not part of the RemoteUserBinding", func() { + By("creating the RemoteUser & RemoteUserBinding for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserLuffyName, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RubAnnotation: "true", + }, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + By("creating a RemoteTarget") + const ( + myLabelKey = "my-label-key" + myLabelValue = "my-label-value" + ) + remoteTarget := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteTargetName5, + Namespace: namespace, + Labels: map[string]string{myLabelKey: myLabelValue}, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repoUrl, + TargetRepository: repoUrl, + UpstreamBranch: branch, + TargetBranch: branch, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTarget) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer") + remotesyncer5 := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName5, + Namespace: namespace, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: branch, + DefaultUnauthorizedUserMode: syngit.Block, + ExcludedFields: []string{".metadata.uid"}, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + RemoteTargetSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{myLabelKey: myLabelValue}, + }, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + admissionv1.Delete, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer5) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap") + Wait3() + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName5, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm, + metav1.CreateOptions{}, + ) + return err != nil && strings.Contains(err.Error(), rtNotFound) + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is not present on the repo") + Wait3() + repo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: branch, + } + exists, err := IsObjectInRepo(*repo, cm) + Expect(err).To(HaveOccurred()) + Expect(exists).To(BeFalse()) + + By("checking that the configmap is not present on the cluster") + nnCm := types.NamespacedName{ + Name: cmName1, + Namespace: namespace, + } + getCm := &corev1.ConfigMap{} + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err != nil && strings.Contains(err.Error(), notPresentOnCluser) + }, timeout, interval).Should(BeTrue()) + + }) +}) diff --git a/test/e2e/syngit/24_remotesyncers_scope_remotetarget_test.go b/test/e2e/syngit/24_remotesyncers_scope_remotetarget_test.go new file mode 100644 index 0000000..81eeb68 --- /dev/null +++ b/test/e2e/syngit/24_remotesyncers_scope_remotetarget_test.go @@ -0,0 +1,190 @@ +/* +Copyright 2024. + +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 e2e_syngit + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + . "github.com/syngit-org/syngit/test/utils" + admissionv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("24 One RemoteTarget scoped by multiple RemoteSyncers", func() { + ctx := context.TODO() + + const ( + remoteUserLuffyName = "remoteuser-luffy" + remoteSyncerName1 = "remotesyncer-test24.1" + remoteSyncerName2 = "remotesyncer-test24.2" + cmName = "test-cm24" + branch = "main" + ) + + It("should deny the RemoteTarget creation", func() { + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + + By("creating the RemoteUser for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserLuffyName, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RubAnnotation: "true", + }, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the first RemoteSyncer") + remotesyncer1 := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName1, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: branch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer1) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the second RemoteSyncer") + remotesyncer2 := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName2, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branch, + }, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: branch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer2) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("deleting the first RemoteSyncer") + Eventually(func() bool { + err := sClient.As(Luffy).Delete(remotesyncer1) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap") + Wait3() + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the repo") + Wait3() + repo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: branch, + } + exists, err := IsObjectInRepo(*repo, cm) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm := types.NamespacedName{ + Name: cmName, + Namespace: namespace, + } + getCm := &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + }) +}) diff --git a/test/e2e/syngit/25_remotetarget_fastforward_merge_test.go b/test/e2e/syngit/25_remotetarget_fastforward_merge_test.go new file mode 100644 index 0000000..47bf42b --- /dev/null +++ b/test/e2e/syngit/25_remotetarget_fastforward_merge_test.go @@ -0,0 +1,408 @@ +/* +Copyright 2024. + +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 e2e_syngit + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + . "github.com/syngit-org/syngit/test/utils" + admissionv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("25 Test fast-forward merge", func() { + ctx := context.TODO() + + const ( + remoteUserLuffyName = "remoteuser-luffy" + remoteSyncerName1 = "remotesyncer-test25.1" + remoteSyncerName2 = "remotesyncer-test25.2" + remoteTargetNameCustomBranch = "remotetarget-test25.1" + remoteTargetNameUpstreamBranch = "remotetarget-test25.2" + remoteUserBindingLuffyName = "remoteuserbinding-luffy" + cmName1 = "test-cm25.1" + cmName2 = "test-cm25.2" + cmName3 = "test-cm25.3" + upstreamBranch = "main" + customBranch = "custom-branch25" + ) + + It("should correctly pull the changes from the upstream", func() { + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + + By("creating the RemoteUser & RemoteUserBinding for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a RemoteTarget targeting the custom branch") + remoteTargetCustomBranch := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteTargetNameCustomBranch, + Namespace: namespace, + Labels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: customBranch, + }, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repoUrl, + TargetRepository: repoUrl, + UpstreamBranch: upstreamBranch, + TargetBranch: customBranch, + MergeStrategy: syngit.TryFastForwardOrDie, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTargetCustomBranch) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteUserBinding with the RemoteUser & RemoteTarget targeting the custom branch") + remoteUserBindingLuffy := &syngit.RemoteUserBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserBindingLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserBindingSpec{ + RemoteUserRefs: []corev1.ObjectReference{ + { + Name: remoteUserLuffyName, + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: remoteTargetNameCustomBranch, + }, + }, + Subject: v1.Subject{ + Kind: "User", + Name: string(Luffy), + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserBindingLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer targeting the custom-branch") + remotesyncer := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName1, + Namespace: namespace, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: upstreamBranch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + RemoteTargetSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: customBranch, + }, + }, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + remotesyncerDeepCopied := remotesyncer.DeepCopy() + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap on the custom-branch") + Wait3() + cm1 := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName1, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm1, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the repo") + Wait3() + customBranchRepo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: customBranch, + } + exists, err := IsObjectInRepo(*customBranchRepo, cm1) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm := types.NamespacedName{ + Name: cmName1, + Namespace: namespace, + } + getCm := &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("deleting the first RemoteSyncer") + delErr := sClient.As(Luffy).Delete(remotesyncer) + Expect(delErr).ToNot(HaveOccurred()) + + By("creating a RemoteTarget targeting the upstream branch") + remoteTargetUpstreamBranch := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteTargetNameUpstreamBranch, + Namespace: namespace, + Labels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: upstreamBranch, + }, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repoUrl, + TargetRepository: repoUrl, + UpstreamBranch: upstreamBranch, + TargetBranch: upstreamBranch, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTargetUpstreamBranch) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("updating the RemoteUserBinding with the RemoteUser & RemoteTarget targeting the upstream branch") + remoteUserBindingLuffy = &syngit.RemoteUserBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserBindingLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserBindingSpec{ + RemoteUserRefs: []corev1.ObjectReference{ + { + Name: remoteUserLuffyName, + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: remoteTargetNameUpstreamBranch, + }, + { + Name: remoteTargetNameCustomBranch, + }, + }, + Subject: v1.Subject{ + Kind: "User", + Name: string(Luffy), + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserBindingLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer targeting the upstream main branch") + remotesyncer2 := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName2, + Namespace: namespace, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: upstreamBranch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + RemoteTargetSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: upstreamBranch, + }, + }, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer2) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating another test configmap on the main branch") + Wait3() + cm2 := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName2, Namespace: namespace}, + Data: map[string]string{"test": "non"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm2, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the repo") + Wait3() + upstreamRepo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: upstreamBranch, + } + exists, err = IsObjectInRepo(*upstreamRepo, cm2) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm2 := types.NamespacedName{ + Name: cmName2, + Namespace: namespace, + } + getCm = &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm2, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("performing a merge from the custom-branch to the main branch") + mergeErr := Merge(*customBranchRepo, customBranch, upstreamBranch) + Expect(mergeErr).ToNot(HaveOccurred()) + + By("deleting the second RemoteSyncer") + delErr = sClient.As(Luffy).Delete(remotesyncer2) + Expect(delErr).ToNot(HaveOccurred()) + + By("re-creating the first RemoteSyncer") + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncerDeepCopied) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating another test configmap on the custom-branch") + Wait3() + cm3 := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName3, Namespace: namespace}, + Data: map[string]string{"test": "non"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm3, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the first custom-branch configmap is present in the custom-branch") + Wait3() + exists, err = IsObjectInRepo(*customBranchRepo, cm1) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the previous upstream configmap is present in the custom-branch") + Wait3() + exists, err = IsObjectInRepo(*customBranchRepo, cm2) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the new configmap is present in the custom-branch") + Wait3() + exists, err = IsObjectInRepo(*customBranchRepo, cm3) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm3 := types.NamespacedName{ + Name: cmName3, + Namespace: namespace, + } + getCm = &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm3, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + }) +}) diff --git a/test/e2e/syngit/26_remotetarget_hardreset_merge_test.go b/test/e2e/syngit/26_remotetarget_hardreset_merge_test.go new file mode 100644 index 0000000..a955e27 --- /dev/null +++ b/test/e2e/syngit/26_remotetarget_hardreset_merge_test.go @@ -0,0 +1,764 @@ +/* +Copyright 2024. + +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 e2e_syngit + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + . "github.com/syngit-org/syngit/test/utils" + admissionv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("26 Test hard-reset merge", func() { + ctx := context.TODO() + + const ( + remoteUserLuffyName = "remoteuser-luffy" + remoteSyncerName1 = "remotesyncer-test26.1" + remoteSyncerName2 = "remotesyncer-test26.2" + remoteSyncerName3 = "remotesyncer-test26.3" + remoteSyncerName4 = "remotesyncer-test26.4" + remoteTargetNameCustomBranch1 = "remotetarget-test26.1" + remoteTargetNameUpstreamBranch1 = "remotetarget-test26.2" + remoteTargetNameCustomBranch2 = "remotetarget-test26.3" + remoteTargetNameUpstreamBranch2 = "remotetarget-test26.4" + remoteUserBindingLuffyName = "remoteuserbinding-luffy" + cmName1 = "test-cm26.1" + cmName2 = "test-cm26.2" + cmName3 = "test-cm26.3" + cmName4 = "test-cm26.4" + cmName5 = "test-cm26.5" + cmName6 = "test-cm26.6" + upstreamBranch = "main" + customBranch = "custom-branch26" + ) + + It("should correctly pull the changes from the upstream", func() { + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + + By("creating the RemoteUser & RemoteUserBinding for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a RemoteTarget targeting the custom branch") + remoteTargetCustomBranch := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteTargetNameCustomBranch1, + Namespace: namespace, + Labels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: customBranch, + }, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repoUrl, + TargetRepository: repoUrl, + UpstreamBranch: upstreamBranch, + TargetBranch: customBranch, + MergeStrategy: syngit.TryHardResetOrDie, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTargetCustomBranch) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteUserBinding with the RemoteUser & RemoteTarget targeting the custom branch") + remoteUserBindingLuffy := &syngit.RemoteUserBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserBindingLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserBindingSpec{ + RemoteUserRefs: []corev1.ObjectReference{ + { + Name: remoteUserLuffyName, + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: remoteTargetNameCustomBranch1, + }, + }, + Subject: v1.Subject{ + Kind: "User", + Name: string(Luffy), + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserBindingLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer targeting the custom-branch") + remotesyncer := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName1, + Namespace: namespace, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: upstreamBranch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + RemoteTargetSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: customBranch, + }, + }, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + remotesyncerDeepCopied := remotesyncer.DeepCopy() + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap on the custom-branch") + Wait3() + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName1, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the repo") + Wait3() + customBranchRepo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: customBranch, + } + exists, err := IsObjectInRepo(*customBranchRepo, cm) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm := types.NamespacedName{ + Name: cmName1, + Namespace: namespace, + } + getCm := &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("deleting the first RemoteSyncer") + delErr := sClient.As(Luffy).Delete(remotesyncer) + Expect(delErr).ToNot(HaveOccurred()) + + By("creating a RemoteTarget targeting the upstream branch") + remoteTargetUpstreamBranch := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteTargetNameUpstreamBranch1, + Namespace: namespace, + Labels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: upstreamBranch, + }, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repoUrl, + TargetRepository: repoUrl, + UpstreamBranch: upstreamBranch, + TargetBranch: upstreamBranch, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTargetUpstreamBranch) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("updating the RemoteUserBinding with the RemoteUser & RemoteTarget targeting the upstream branch") + remoteUserBindingLuffy = &syngit.RemoteUserBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserBindingLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserBindingSpec{ + RemoteUserRefs: []corev1.ObjectReference{ + { + Name: remoteUserLuffyName, + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: remoteTargetNameUpstreamBranch1, + }, + { + Name: remoteTargetNameCustomBranch1, + }, + }, + Subject: v1.Subject{ + Kind: "User", + Name: string(Luffy), + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserBindingLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer targeting the upstream main branch") + remotesyncer2 := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName2, + Namespace: namespace, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: upstreamBranch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + RemoteTargetSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: upstreamBranch, + }, + }, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer2) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating another test configmap on the main branch") + Wait3() + cm2 := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName2, Namespace: namespace}, + Data: map[string]string{"test": "non"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm2, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the repo") + Wait3() + upstreamRepo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: upstreamBranch, + } + exists, err = IsObjectInRepo(*upstreamRepo, cm2) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm2 := types.NamespacedName{ + Name: cmName2, + Namespace: namespace, + } + getCm = &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm2, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("performing a merge from the custom-branch to the main branch") + mergeErr := Merge(*customBranchRepo, customBranch, upstreamBranch) + Expect(mergeErr).ToNot(HaveOccurred()) + + By("deleting the second RemoteSyncer") + delErr = sClient.As(Luffy).Delete(remotesyncer2) + Expect(delErr).ToNot(HaveOccurred()) + + By("re-creating the first RemoteSyncer") + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncerDeepCopied) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating another test configmap on the custom-branch") + Wait3() + cm3 := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName3, Namespace: namespace}, + Data: map[string]string{"test": "non"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm3, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the previous upstream configmap is present in the custom-branch") + Wait3() + exists, err = IsObjectInRepo(*customBranchRepo, cm2) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the new configmap is present in the custom-branch") + Wait3() + exists, err = IsObjectInRepo(*customBranchRepo, cm3) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm3 := types.NamespacedName{ + Name: cmName3, + Namespace: namespace, + } + getCm = &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm3, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + }) + + It("should overwrite the custom branch's commit by the upstream's branch one", func() { //nolint:dupl + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + + By("creating the RemoteUser & RemoteUserBinding for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a RemoteTarget targeting the custom branch") + remoteTargetCustomBranch := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteTargetNameCustomBranch2, + Namespace: namespace, + Labels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: customBranch, + }, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repoUrl, + TargetRepository: repoUrl, + UpstreamBranch: upstreamBranch, + TargetBranch: customBranch, + MergeStrategy: syngit.TryHardResetOrDie, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTargetCustomBranch) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteUserBinding with the RemoteUser & RemoteTarget targeting the custom branch") + remoteUserBindingLuffy := &syngit.RemoteUserBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserBindingLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserBindingSpec{ + RemoteUserRefs: []corev1.ObjectReference{ + { + Name: remoteUserLuffyName, + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: remoteTargetNameCustomBranch2, + }, + }, + Subject: v1.Subject{ + Kind: "User", + Name: string(Luffy), + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserBindingLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer targeting the custom-branch") + remotesyncer := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName3, + Namespace: namespace, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: upstreamBranch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + RemoteTargetSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: customBranch, + }, + }, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + remotesyncerDeepCopied := remotesyncer.DeepCopy() + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap on the custom-branch") + Wait3() + cm1 := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName4, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm1, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the repo") + Wait3() + customBranchRepo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: customBranch, + } + exists, err := IsObjectInRepo(*customBranchRepo, cm1) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm := types.NamespacedName{ + Name: cmName4, + Namespace: namespace, + } + getCm := &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("deleting the first RemoteSyncer") + delErr := sClient.As(Luffy).Delete(remotesyncer) + Expect(delErr).ToNot(HaveOccurred()) + + By("creating a RemoteTarget targeting the upstream branch") + remoteTargetUpstreamBranch := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteTargetNameUpstreamBranch2, + Namespace: namespace, + Labels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: upstreamBranch, + }, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repoUrl, + TargetRepository: repoUrl, + UpstreamBranch: upstreamBranch, + TargetBranch: upstreamBranch, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTargetUpstreamBranch) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("updating the RemoteUserBinding with the RemoteUser & RemoteTarget targeting the upstream branch") + remoteUserBindingLuffy = &syngit.RemoteUserBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserBindingLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserBindingSpec{ + RemoteUserRefs: []corev1.ObjectReference{ + { + Name: remoteUserLuffyName, + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: remoteTargetNameUpstreamBranch2, + }, + { + Name: remoteTargetNameCustomBranch2, + }, + }, + Subject: v1.Subject{ + Kind: "User", + Name: string(Luffy), + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserBindingLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer targeting the upstream main branch") + remotesyncer2 := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName4, + Namespace: namespace, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: upstreamBranch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + RemoteTargetSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: upstreamBranch, + }, + }, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer2) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating another test configmap on the main branch") + Wait3() + cm2 := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName5, Namespace: namespace}, + Data: map[string]string{"test": "non"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm2, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the repo") + Wait3() + upstreamRepo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: upstreamBranch, + } + exists, err = IsObjectInRepo(*upstreamRepo, cm2) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm2 := types.NamespacedName{ + Name: cmName5, + Namespace: namespace, + } + getCm = &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm2, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("deleting the second RemoteSyncer") + delErr = sClient.As(Luffy).Delete(remotesyncer2) + Expect(delErr).ToNot(HaveOccurred()) + + By("re-creating the first RemoteSyncer") + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncerDeepCopied) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating another test configmap on the custom-branch") + Wait3() + cm3 := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName6, Namespace: namespace}, + Data: map[string]string{"test": "non"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm3, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the first upstream configmap is not present in the custom-branch") + Wait3() + exists, err = IsObjectInRepo(*customBranchRepo, cm1) + Expect(err).To(HaveOccurred()) + Expect(exists).To(BeFalse()) + + By("checking that the previous upstream configmap is present in the custom-branch") + Wait3() + exists, err = IsObjectInRepo(*customBranchRepo, cm2) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the new configmap is present in the custom-branch") + Wait3() + exists, err = IsObjectInRepo(*customBranchRepo, cm3) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm3 := types.NamespacedName{ + Name: cmName6, + Namespace: namespace, + } + getCm = &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm3, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + }) +}) diff --git a/test/e2e/syngit/27_remotetarget_fastforward_hardreset_merge_test.go b/test/e2e/syngit/27_remotetarget_fastforward_hardreset_merge_test.go new file mode 100644 index 0000000..9139767 --- /dev/null +++ b/test/e2e/syngit/27_remotetarget_fastforward_hardreset_merge_test.go @@ -0,0 +1,404 @@ +/* +Copyright 2024. + +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 e2e_syngit + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + . "github.com/syngit-org/syngit/test/utils" + admissionv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("27 Test fast-forward or hard-reset merge", func() { + ctx := context.TODO() + + const ( + remoteUserLuffyName = "remoteuser-luffy" + remoteSyncerName1 = "remotesyncer-test27.1" + remoteSyncerName2 = "remotesyncer-test27.2" + remoteTargetNameCustomBranch = "remotetarget-test27.1" + remoteTargetNameUpstreamBranch = "remotetarget-test27.2" + remoteUserBindingLuffyName = "remoteuserbinding-luffy" + cmName1 = "test-cm27.1" + cmName2 = "test-cm27.2" + cmName3 = "test-cm27.3" + upstreamBranch = "main" + customBranch = "custom-branch27" + ) + + It("should try fast-forward and use hard-reset", func() { //nolint:dupl + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + + By("creating the RemoteUser & RemoteUserBinding for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a RemoteTarget targeting the custom branch") + remoteTargetCustomBranch := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteTargetNameCustomBranch, + Namespace: namespace, + Labels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: customBranch, + }, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repoUrl, + TargetRepository: repoUrl, + UpstreamBranch: upstreamBranch, + TargetBranch: customBranch, + MergeStrategy: syngit.TryFastForwardOrHardReset, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTargetCustomBranch) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteUserBinding with the RemoteUser & RemoteTarget targeting the custom branch") + remoteUserBindingLuffy := &syngit.RemoteUserBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserBindingLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserBindingSpec{ + RemoteUserRefs: []corev1.ObjectReference{ + { + Name: remoteUserLuffyName, + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: remoteTargetNameCustomBranch, + }, + }, + Subject: v1.Subject{ + Kind: "User", + Name: string(Luffy), + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserBindingLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer targeting the custom-branch") + remotesyncer := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName1, + Namespace: namespace, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: upstreamBranch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + RemoteTargetSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: customBranch, + }, + }, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + remotesyncerDeepCopied := remotesyncer.DeepCopy() + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap on the custom-branch") + Wait3() + cm1 := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName1, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm1, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the repo") + Wait3() + customBranchRepo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: customBranch, + } + exists, err := IsObjectInRepo(*customBranchRepo, cm1) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm := types.NamespacedName{ + Name: cmName1, + Namespace: namespace, + } + getCm := &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("deleting the first RemoteSyncer") + delErr := sClient.As(Luffy).Delete(remotesyncer) + Expect(delErr).ToNot(HaveOccurred()) + + By("creating a RemoteTarget targeting the upstream branch") + remoteTargetUpstreamBranch := &syngit.RemoteTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteTargetNameUpstreamBranch, + Namespace: namespace, + Labels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: upstreamBranch, + }, + }, + Spec: syngit.RemoteTargetSpec{ + UpstreamRepository: repoUrl, + TargetRepository: repoUrl, + UpstreamBranch: upstreamBranch, + TargetBranch: upstreamBranch, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteTargetUpstreamBranch) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("updating the RemoteUserBinding with the RemoteUser & RemoteTarget targeting the upstream branch") + remoteUserBindingLuffy = &syngit.RemoteUserBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteUserBindingLuffyName, + Namespace: namespace, + }, + Spec: syngit.RemoteUserBindingSpec{ + RemoteUserRefs: []corev1.ObjectReference{ + { + Name: remoteUserLuffyName, + }, + }, + RemoteTargetRefs: []corev1.ObjectReference{ + { + Name: remoteTargetNameUpstreamBranch, + }, + { + Name: remoteTargetNameCustomBranch, + }, + }, + Subject: v1.Subject{ + Kind: "User", + Name: string(Luffy), + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserBindingLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteSyncer targeting the upstream main branch") + remotesyncer2 := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName1, + Namespace: namespace, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: upstreamBranch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.OneTarget, + RemoteRepository: repoUrl, + RemoteTargetSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + syngit.ManagedByLabelKey: syngit.ManagedByLabelValue, + syngit.RtLabelBranchKey: upstreamBranch, + }, + }, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer2) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating another test configmap on the main branch") + Wait3() + cm2 := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName2, Namespace: namespace}, + Data: map[string]string{"test": "non"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm2, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the repo") + Wait3() + upstreamRepo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: upstreamBranch, + } + exists, err = IsObjectInRepo(*upstreamRepo, cm2) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm2 := types.NamespacedName{ + Name: cmName2, + Namespace: namespace, + } + getCm = &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm2, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("deleting the second RemoteSyncer") + delErr = sClient.As(Luffy).Delete(remotesyncer2) + Expect(delErr).ToNot(HaveOccurred()) + + By("re-creating the first RemoteSyncer") + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncerDeepCopied) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating another test configmap on the custom-branch") + Wait3() + cm3 := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName3, Namespace: namespace}, + Data: map[string]string{"test": "non"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm3, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the first upstream configmap is not present in the custom-branch") + Wait3() + exists, err = IsObjectInRepo(*customBranchRepo, cm1) + Expect(err).To(HaveOccurred()) + Expect(exists).To(BeFalse()) + + By("checking that the previous upstream configmap is present in the custom-branch") + Wait3() + exists, err = IsObjectInRepo(*customBranchRepo, cm2) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the new configmap is present in the custom-branch") + Wait3() + exists, err = IsObjectInRepo(*customBranchRepo, cm3) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm3 := types.NamespacedName{ + Name: cmName3, + Namespace: namespace, + } + getCm = &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm3, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + }) +}) diff --git a/test/e2e/syngit/28_remoteuser_created_after_test.go b/test/e2e/syngit/28_remoteuser_created_after_test.go new file mode 100644 index 0000000..78661dd --- /dev/null +++ b/test/e2e/syngit/28_remoteuser_created_after_test.go @@ -0,0 +1,173 @@ +/* +Copyright 2024. + +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 e2e_syngit + +import ( + "context" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + . "github.com/syngit-org/syngit/test/utils" + admissionv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("28 RemoteUser created after RemoteSyncer & RemoteTargets", func() { + ctx := context.TODO() + + const ( + remoteUserLuffyName = "remoteuser-luffy" + remoteUserBindingLuffyName = "remoteuserbinding-luffy" + remoteSyncerName = "remotesyncer-test28" + cmName = "test-cm28" + upstreamBranch = "main" + branch1 = "branch28.1" + branch2 = "branch28.2" + ) + + It("should associate the managed RemoteTargets to the new RemoteUser", func() { + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + branches := strings.Join([]string{branch1, branch2}, ", ") + By("creating the RemoteSyncer") + remotesyncer := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branches, + syngit.RtAnnotationUserSpecificKey: string(syngit.RtAnnotationOneUserOneBranchValue), + }, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: upstreamBranch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.MultipleTarget, + RemoteRepository: repoUrl, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating the RemoteUser & RemoteUserBinding for Luffy after the RemoteSyncer & RemoteTargets creation") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: "remoteuser-luffy", + Namespace: namespace, + Annotations: map[string]string{ + syngit.RubAnnotation: "true", + }, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap") + Wait3() + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the branches") + Wait3() + repo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: branch1, + } + exists, err := IsObjectInRepo(*repo, cm) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + Wait3() + repo = &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: branch2, + } + exists, err = IsObjectInRepo(*repo, cm) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + Wait3() + repo = &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: string(Luffy), + } + exists, err = IsObjectInRepo(*repo, cm) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("checking that the configmap is present on the cluster") + nnCm := types.NamespacedName{ + Name: cmName, + Namespace: namespace, + } + getCm := &corev1.ConfigMap{} + + Eventually(func() bool { + err := sClient.As(Luffy).Get(nnCm, getCm) + return err == nil + }, timeout, interval).Should(BeTrue()) + + }) +}) diff --git a/test/e2e/syngit/29_pattern_remove_test.go b/test/e2e/syngit/29_pattern_remove_test.go new file mode 100644 index 0000000..fb1c0c3 --- /dev/null +++ b/test/e2e/syngit/29_pattern_remove_test.go @@ -0,0 +1,410 @@ +/* +Copyright 2024. + +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 e2e_syngit + +import ( + "context" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + syngit "github.com/syngit-org/syngit/pkg/api/v1beta3" + . "github.com/syngit-org/syngit/test/utils" + admissionv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("29 Add & remove patterns tests", func() { + ctx := context.TODO() + + const ( + remoteUserLuffyName = "remoteuser-luffy" + remoteSyncerName1 = "remotesyncer-test29.2" + remoteSyncerName2 = "remotesyncer-test29.2" + upstreamBranch = "main" + branch1 = "branch29.1" + branch2 = "branch29.2" + cmName1 = "test-cm29.1" + cmName2 = "test-cm29.2" + cmName3 = "test-cm29.3" + cmName4 = "test-cm29.4" + cmName5 = "test-cm29.5" + ) + + It("push on the right branches at the right moment", func() { + + By("creating the RemoteUser & RemoteUserBinding for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: "remoteuser-luffy", + Namespace: namespace, + Annotations: map[string]string{ + syngit.RubAnnotation: "true", + }, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + branches := strings.Join([]string{branch1, branch2}, ", ") + By("creating the RemoteSyncer that target everybranches") + remotesyncer := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName1, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationOneOrManyBranchesKey: branches, + syngit.RtAnnotationUserSpecificKey: string(syngit.RtAnnotationOneUserOneBranchValue), + }, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: upstreamBranch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.MultipleTarget, + RemoteRepository: repoUrl, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap that should be push everywhere") + Wait3() + cm1 := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName1, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm1, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the branches") + Wait3() + repo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: branch1, + } + exists, err := IsObjectInRepo(*repo, cm1) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + Wait3() + repo = &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: branch2, + } + exists, err = IsObjectInRepo(*repo, cm1) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + Wait3() + repo = &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: string(Luffy), + } + exists, err = IsObjectInRepo(*repo, cm1) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("updating the RemoteSyncer to have only user specific pattern") + remotesyncer.Annotations[syngit.RtAnnotationOneOrManyBranchesKey] = "" + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap that should be push only on the user specific branch") + Wait3() + cm2 := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName2, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm2, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the user branch and not on the other") + Wait3() + repo = &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: branch1, + } + exists, err = IsObjectInRepo(*repo, cm2) + Expect(exists).To(BeFalse()) + Expect(err).To(HaveOccurred()) + Wait3() + repo = &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: branch2, + } + exists, err = IsObjectInRepo(*repo, cm2) + Expect(err).To(HaveOccurred()) + Expect(exists).To(BeFalse()) + Wait3() + repo = &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: string(Luffy), + } + exists, err = IsObjectInRepo(*repo, cm2) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("updating the RemoteSyncer to not have any pattern") + remotesyncer.Annotations[syngit.RtAnnotationUserSpecificKey] = "" + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap that should not be push on any branch") + Wait3() + cm3 := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName3, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm3, + metav1.CreateOptions{}, + ) + return err != nil && strings.Contains(err.Error(), rtNotFound) + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is not present on any branch") + Wait3() + repo = &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: branch1, + } + exists, err = IsObjectInRepo(*repo, cm3) + Expect(err).To(HaveOccurred()) + Expect(exists).To(BeFalse()) + Wait3() + repo = &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: branch2, + } + exists, err = IsObjectInRepo(*repo, cm3) + Expect(err).To(HaveOccurred()) + Expect(exists).To(BeFalse()) + Wait3() + repo = &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: string(Luffy), + } + exists, err = IsObjectInRepo(*repo, cm3) + Expect(err).To(HaveOccurred()) + Expect(exists).To(BeFalse()) + + }) + + It("push not push on the user branch when the RemoteUserBinding is not managed anymore", func() { + + By("creating the RemoteUser & RemoteUserBinding for Luffy") + luffySecretName := string(Luffy) + "-creds" + remoteUserLuffy := &syngit.RemoteUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: "remoteuser-luffy", + Namespace: namespace, + Annotations: map[string]string{ + syngit.RubAnnotation: "true", + }, + }, + Spec: syngit.RemoteUserSpec{ + Email: "sample@email.com", + GitBaseDomainFQDN: gitP1Fqdn, + SecretRef: corev1.SecretReference{ + Name: luffySecretName, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + repoUrl := "https://" + gitP1Fqdn + "/syngituser/blue.git" + By("creating the RemoteSyncer that target everybranches") + remotesyncer := &syngit.RemoteSyncer{ + ObjectMeta: metav1.ObjectMeta{ + Name: remoteSyncerName2, + Namespace: namespace, + Annotations: map[string]string{ + syngit.RtAnnotationUserSpecificKey: string(syngit.RtAnnotationOneUserOneBranchValue), + }, + }, + Spec: syngit.RemoteSyncerSpec{ + InsecureSkipTlsVerify: true, + DefaultBranch: upstreamBranch, + DefaultUnauthorizedUserMode: syngit.Block, + Strategy: syngit.CommitApply, + TargetStrategy: syngit.MultipleTarget, + RemoteRepository: repoUrl, + ScopedResources: syngit.ScopedResources{ + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + }, + }, + }, + }, + } + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remotesyncer) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap that should be push on the user specific branch") + Wait3() + cm1 := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName4, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm1, + metav1.CreateOptions{}, + ) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is present on the user specific branch") + Wait3() + repo := &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: string(Luffy), + } + exists, err := IsObjectInRepo(*repo, cm1) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + By("updating the RemoteUser to remove the association pattern") + remoteUserLuffy.Annotations[syngit.RubAnnotation] = "false" + Eventually(func() bool { + err := sClient.As(Luffy).CreateOrUpdate(remoteUserLuffy) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("creating a test configmap that should not be pushed on the user specific branch") + Wait3() + cm2 := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: cmName5, Namespace: namespace}, + Data: map[string]string{"test": "oui"}, + } + Eventually(func() bool { + _, err := sClient.KAs(Luffy).CoreV1().ConfigMaps(namespace).Create(ctx, + cm2, + metav1.CreateOptions{}, + ) + return err != nil && strings.Contains(err.Error(), ruNotFound) + }, timeout, interval).Should(BeTrue()) + + By("checking that the configmap is not present on the user specific branch") + Wait3() + repo = &Repo{ + Fqdn: gitP1Fqdn, + Owner: "syngituser", + Name: "blue", + Branch: string(Luffy), + } + exists, err = IsObjectInRepo(*repo, cm2) + Expect(err).To(HaveOccurred()) + Expect(exists).To(BeFalse()) + + }) + +}) diff --git a/test/e2e/syngit/e2e_suite_test.go b/test/e2e/syngit/e2e_suite_test.go index 4331b63..1d40eab 100644 --- a/test/e2e/syngit/e2e_suite_test.go +++ b/test/e2e/syngit/e2e_suite_test.go @@ -70,8 +70,13 @@ const ( x509ErrorMessage = "x509: certificate signed by unknown authority" crossRubErrorMessage = "the RemoteUser is already bound in the RemoteUserBinding" rubNotFound = "no RemoteUserBinding found for the user" - defaultUserNotFound = "the default user is not found" + defaultUserNotFound = "the default RemoteUser is not found" + defaultTargetNotFound = "the default RemoteTarget is not found" notPresentOnCluser = "not found" + sameBranchRepo = "should not be set when the target repo & target branch are the same as the upstream repo & branch" + rtNotFound = "no RemoteTarget found" + ruNotFound = "no RemoteUser found" + oneTargetForMultipleMessage = "multiple RemoteTargets found for OneTarget set as the TargetStrategy in the RemoteSyncer" ) // CMD & CLIENT @@ -205,6 +210,8 @@ func setupManager() { Expect(errWebhook).NotTo(HaveOccurred()) errWebhook = webhooksyngitv1beta3.SetupRemoteUserBindingWebhookWithManager(k8sManager) Expect(errWebhook).NotTo(HaveOccurred()) + errWebhook = webhooksyngitv1beta3.SetupRemoteTargetWebhookWithManager(k8sManager) + Expect(errWebhook).NotTo(HaveOccurred()) k8sManager.GetWebhookServer().Register("/syngit-v1beta3-remoteuser-association", &webhook.Admission{Handler: &webhooksyngitv1beta3.RemoteUserAssociationWebhookHandler{ Client: k8sManager.GetClient(), Decoder: admission.NewDecoder(k8sManager.GetScheme()), @@ -221,6 +228,10 @@ func setupManager() { Client: k8sManager.GetClient(), Decoder: admission.NewDecoder(k8sManager.GetScheme()), }}) + k8sManager.GetWebhookServer().Register("/syngit-v1beta3-remotesyncer-target-pattern", &webhook.Admission{Handler: &webhooksyngitv1beta3.RemoteSyncerTargetPatternWebhookHandler{ + Client: k8sManager.GetClient(), + Decoder: admission.NewDecoder(k8sManager.GetScheme()), + }}) By("setting up the controllers") errController := (&controllerssyngit.RemoteUserReconciler{ @@ -238,6 +249,11 @@ func setupManager() { Scheme: k8sManager.GetScheme(), }).SetupWithManager(k8sManager) Expect(errController).ToNot(HaveOccurred()) + errController = (&controllerssyngit.RemoteTargetReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + }).SetupWithManager(k8sManager) + Expect(errController).ToNot(HaveOccurred()) By("starting the manager") go func() { @@ -485,11 +501,20 @@ func deleteRbac(ctx context.Context) { } +func deleteRepos() { + By("reseting the gitea repos") + cmd := exec.Command("make", "reset-gitea") + _, err := Run(cmd) + Expect(err).NotTo(HaveOccurred()) +} + var _ = AfterSuite(func() { ctx := context.TODO() deleteRbac(ctx) + deleteRepos() + By("tearing down the test environment") Eventually(func() bool { errTestEnv := testEnv.Stop() @@ -505,9 +530,28 @@ var _ = AfterSuite(func() { var _ = AfterEach(func() { ctx := context.TODO() + By(fmt.Sprintf("deleting the remotetargets from the %s ns", namespace)) + remoteTargets := &syngit.RemoteTargetList{} + err := sClient.As(Admin).List(namespace, remoteTargets) + if err == nil { + for _, remotetarget := range remoteTargets.Items { + nnRub := types.NamespacedName{ + Name: remotetarget.Name, + Namespace: remotetarget.Namespace, + } + rub := &syngit.RemoteTarget{} + err = sClient.As(Admin).Get(nnRub, rub) + Expect(err).NotTo(HaveOccurred()) + Eventually(func() bool { + err := sClient.As(Admin).Delete(rub) + return err == nil + }, timeout, interval).Should(BeTrue()) + } + } + By(fmt.Sprintf("deleting the remotesyncers from the %s ns", namespace)) remoteSyncers := &syngit.RemoteSyncerList{} - err := sClient.As(Admin).List(namespace, remoteSyncers) + err = sClient.As(Admin).List(namespace, remoteSyncers) if err == nil { for _, remotesyncer := range remoteSyncers.Items { nnRs := types.NamespacedName{ @@ -588,6 +632,8 @@ var _ = AfterEach(func() { } } + deleteRepos() + }) // Wait3 sleeps for 3 seconds diff --git a/test/utils/gitea.go b/test/utils/gitea.go index dd677ae..528681d 100644 --- a/test/utils/gitea.go +++ b/test/utils/gitea.go @@ -24,7 +24,9 @@ import ( "errors" "fmt" "io" + "math/rand/v2" "net/http" + "strconv" "strings" "time" @@ -35,9 +37,10 @@ import ( ) type Repo struct { - Fqdn string - Owner string - Name string + Fqdn string + Owner string + Name string + Branch string } type Tree struct { @@ -74,7 +77,7 @@ func getAdminToken(baseFqdn string) (string, error) { url := fmt.Sprintf("https://%s/api/v1/users/%s/tokens", baseFqdn, username) // Prepare the request payload - tokenName := "admin-e2e-token" + tokenName := fmt.Sprintf("%s%c", "admin-e2e-token", rand.IntN(1000)) payload := map[string]interface{}{ "name": tokenName, "scopes": []string{"all"}, @@ -159,9 +162,6 @@ func getTree(repoFqdn string, repoOwner string, repoName string, sha string) ([] } defer resp.Body.Close() - if err != nil { - return nil, fmt.Errorf("failed to get repo tree: %v", err) - } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { @@ -346,3 +346,97 @@ func getObjectMetadata(obj runtime.Object) (metav1.Object, error) { } return metadata, nil } + +func merge(repo Repo, sourceBranch string, targetBranch string) error { + prUrl := fmt.Sprintf("https://%s/api/v1/repos/%s/%s/pulls", repo.Fqdn, repo.Owner, repo.Name) + + // CREATE PULL REQUEST + data := map[string]interface{}{ + "head": sourceBranch, + "base": targetBranch, + "title": fmt.Sprintf("Merge %s into %s", sourceBranch, targetBranch), + "body": "This pull request was created programmatically.", + } + + body, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("failed to marshal request body: %w", err) + } + + req, err := http.NewRequest("POST", prUrl, bytes.NewBuffer(body)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + apiToken, err := getAdminToken(repo.Fqdn) + if err != nil { + return fmt.Errorf("failed to get the apiToken") + } + req.Header.Set("Authorization", "token "+apiToken) + req.Header.Set("Content-Type", "application/json") + + // Skip Tls verify + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, // Skip TLS verification + }, + } + + // Send the request + client := &http.Client{Transport: tr} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + bodyBytes, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed to create pull request: %s", string(bodyBytes)) + } + + var response map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return fmt.Errorf("failed to parse response: %w", err) + } + + pullUrl := strings.Split(response["html_url"].(string), "/") + pullID, _ := strconv.Atoi(pullUrl[len(pullUrl)-1]) + + // MERGE PULL REQUEST + mergeUrl := fmt.Sprintf("https://%s/api/v1/repos/%s/%s/pulls/%d/merge", repo.Fqdn, repo.Owner, repo.Name, pullID) + + data = map[string]interface{}{ + "do": "merge", + "merge_title": "Merging branches programmatically", + "merge_message": "Merge completed using Gitea API", + } + + body, err = json.Marshal(data) + if err != nil { + return fmt.Errorf("failed to marshal request body: %w", err) + } + + req, err = http.NewRequest("POST", mergeUrl, bytes.NewBuffer(body)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", "token "+apiToken) + req.Header.Set("Content-Type", "application/json") + + // Send the request + client = &http.Client{Transport: tr} + resp, err = client.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed to merge pull request: %s", string(bodyBytes)) + } + + return nil +} diff --git a/test/utils/gitea/delete-gitea-repos.sh b/test/utils/gitea/delete-gitea-repos.sh new file mode 100755 index 0000000..5f34a12 --- /dev/null +++ b/test/utils/gitea/delete-gitea-repos.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# Get the Gitea service NodePort +SERVICE_PORT=$(kubectl get svc $SERVICE_NAME -n $NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}") +NODE_IP=$(kubectl get nodes -o jsonpath="{.items[0].status.addresses[?(@.type=='InternalIP')].address}") + +# Formulate the Gitea URL for API access +GITEA_URL="https://$NODE_IP:$SERVICE_PORT" + +# Create an admin user using Gitea CLI inside the Gitea pod +POD_NAME=$(kubectl get pods -n $NAMESPACE -l app.kubernetes.io/name=gitea -o jsonpath="{.items[0].metadata.name}") +kubectl exec -i $POD_NAME -n $NAMESPACE -- gitea admin user create \ + --username $ADMIN_USERNAME \ + --password "1$ADMIN_PASSWORD" \ + --email $ADMIN_EMAIL \ + --admin 2>&1 > /dev/null + +kubectl exec -i $POD_NAME -n $NAMESPACE -- gitea admin user change-password \ + --username $ADMIN_USERNAME \ + --password $ADMIN_PASSWORD \ + --must-change-password=false 2>&1 > /dev/null + +TOKEN_RESPONSE=$(kubectl exec -i $POD_NAME -n $NAMESPACE -- gitea admin user generate-access-token \ + --username $ADMIN_USERNAME \ + --scopes "all" \ + --token-name mytoken-$RANDOM 2>/dev/null) + +if [ "$TOKEN_RESPONSE" == "null" ]; then + echo "Failed to generate token for syngituser user." + exit 1 +fi + +GIT_TOKEN=$(echo "$TOKEN_RESPONSE" | sed -E 's/.*Access token was successfully created: ([a-f0-9]{40}).*/\1/') + +# +# Delete the blue repo +# +DELETE_REPO_ENDPOINT="$GITEA_URL/api/v1/repos/$ADMIN_USERNAME/blue" + +# Make the API call to create the repository +response=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE -k \ + -H "Content-Type: application/json" \ + "$DELETE_REPO_ENDPOINT?access_token=$GIT_TOKEN") + +# Check the response code +if [ "$response" != 204 ]; then + echo "Failed to DELETE repository. HTTP status code: $response" + exit 1 +fi + +# +# Delete the green repo +# +DELETE_REPO_ENDPOINT="$GITEA_URL/api/v1/repos/$ADMIN_USERNAME/green" + +# Make the API call to create the repository +response=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE -k \ + -H "Content-Type: application/json" \ + "$DELETE_REPO_ENDPOINT?access_token=$GIT_TOKEN") + +# Check the response code +if [ "$response" != 204 ]; then + echo "Failed to DELETE repository. HTTP status code: $response" + exit 1 +fi + +kubectl exec -i $POD_NAME -n $NAMESPACE -- rm -rf /data/git/gitea-repositories \ No newline at end of file diff --git a/test/utils/gitea/gitea-gen-cert.sh b/test/utils/gitea/gitea-gen-cert.sh index 36a0f32..7d8170b 100755 --- a/test/utils/gitea/gitea-gen-cert.sh +++ b/test/utils/gitea/gitea-gen-cert.sh @@ -2,8 +2,8 @@ mkdir -p $GITEA_TEMP_CERT_DIR -kubectl delete secret tls gitea-tls-secret -n $PLATFORM1 || true -kubectl delete secret tls gitea-tls-secret -n $PLATFORM2 || true +kubectl delete secret gitea-tls-secret -n $PLATFORM1 || true +kubectl delete secret gitea-tls-secret -n $PLATFORM2 || true openssl genrsa -out $GITEA_TEMP_CERT_DIR/ca.key 2048 openssl req -x509 -new -nodes -key $GITEA_TEMP_CERT_DIR/ca.key -sha256 -days 365 \ diff --git a/test/utils/gitea/launch-gitea-setup.sh b/test/utils/gitea/launch-gitea-setup.sh index 91fbadd..ba0ab1a 100755 --- a/test/utils/gitea/launch-gitea-setup.sh +++ b/test/utils/gitea/launch-gitea-setup.sh @@ -20,18 +20,13 @@ $PREFIXED_PATH/gitea-gen-cert.sh $NODE_IP export NAMESPACE="$PLATFORM1" export VALUES_FILE="$PREFIXED_PATH/helm-values-$PLATFORM1.yaml" $PREFIXED_PATH/setup-gitea-install.sh -$PREFIXED_PATH/setup-gitea-repos.sh export NAMESPACE="$PLATFORM2" export VALUES_FILE="$PREFIXED_PATH/helm-values-$PLATFORM2.yaml" $PREFIXED_PATH/setup-gitea-install.sh -$PREFIXED_PATH/setup-gitea-repos.sh # Setup users $PREFIXED_PATH/setup-gitea-users.sh -# Bind gitea user -export NAMESPACE="$PLATFORM1" -$PREFIXED_PATH/setup-gitea-bind-platform1.sh -export NAMESPACE="$PLATFORM2" -$PREFIXED_PATH/setup-gitea-bind-platform2.sh \ No newline at end of file +# Setup repos & repos bindings +$PREFIXED_PATH/reset-gitea-repos.sh \ No newline at end of file diff --git a/test/utils/gitea/reset-gitea-repos.sh b/test/utils/gitea/reset-gitea-repos.sh new file mode 100755 index 0000000..ef394b7 --- /dev/null +++ b/test/utils/gitea/reset-gitea-repos.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -a +. test/utils/.env +set +a + +export PREFIXED_PATH=./test/utils/gitea + +export NAMESPACE="$PLATFORM1" +$PREFIXED_PATH/delete-gitea-repos.sh +$PREFIXED_PATH/setup-gitea-repos.sh + +export NAMESPACE="$PLATFORM2" +$PREFIXED_PATH/delete-gitea-repos.sh +$PREFIXED_PATH/setup-gitea-repos.sh + +# Bind gitea user +export NAMESPACE="$PLATFORM1" +$PREFIXED_PATH/setup-gitea-bind-platform1.sh +export NAMESPACE="$PLATFORM2" +$PREFIXED_PATH/setup-gitea-bind-platform2.sh \ No newline at end of file diff --git a/test/utils/gitea/setup-gitea-bind-platform1.sh b/test/utils/gitea/setup-gitea-bind-platform1.sh index 2be288b..db8a094 100755 --- a/test/utils/gitea/setup-gitea-bind-platform1.sh +++ b/test/utils/gitea/setup-gitea-bind-platform1.sh @@ -10,7 +10,7 @@ POD_NAME=$(kubectl get pods -n $NAMESPACE -l app.kubernetes.io/name=gitea -o jso TOKEN_RESPONSE=$(kubectl exec -i $POD_NAME -n $NAMESPACE -- gitea admin user generate-access-token \ --username $ADMIN_USERNAME \ --scopes "all" \ - --token-name bindtoken 2>/dev/null) + --token-name bindtoken-$RANDOM 2>/dev/null) if [ "$TOKEN_RESPONSE" == "null" ]; then echo "Failed to generate token for $ADMIN_USERNAME user." diff --git a/test/utils/gitea/setup-gitea-repos.sh b/test/utils/gitea/setup-gitea-repos.sh index 7b03561..b8e30ef 100755 --- a/test/utils/gitea/setup-gitea-repos.sh +++ b/test/utils/gitea/setup-gitea-repos.sh @@ -23,7 +23,7 @@ kubectl exec -i $POD_NAME -n $NAMESPACE -- gitea admin user change-password \ TOKEN_RESPONSE=$(kubectl exec -i $POD_NAME -n $NAMESPACE -- gitea admin user generate-access-token \ --username $ADMIN_USERNAME \ --scopes "all" \ - --token-name mytoken 2>/dev/null) + --token-name mytoken-$RANDOM 2>/dev/null) if [ "$TOKEN_RESPONSE" == "null" ]; then echo "Failed to generate token for syngituser user." diff --git a/test/utils/syngit.go b/test/utils/syngit.go index 24ad3e3..3d5644e 100644 --- a/test/utils/syngit.go +++ b/test/utils/syngit.go @@ -11,6 +11,10 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +func Merge(repo Repo, sourceBranch string, targetBranch string) error { + return merge(repo, sourceBranch, targetBranch) +} + func GetGiteaURL(namespace string) (string, error) { // Run kubectl to get the NodePort of the gitea service in the given namespace port, err := exec.Command("kubectl", "get", "svc", "gitea-http", "-n", namespace, "-o", "jsonpath={.spec.ports[0].nodePort}").Output() @@ -119,5 +123,9 @@ func GetLatestCommit(repoUrl string, repoOwner string, repoName string) (*Commit } func GetRepoTree(repo Repo) ([]Tree, error) { - return getTree(repo.Fqdn, repo.Owner, repo.Name, "main") + branch := "main" + if repo.Branch != "" { + branch = repo.Branch + } + return getTree(repo.Fqdn, repo.Owner, repo.Name, branch) }