From d56176a9382926332dc1fe64ac7adf8f83c07f7e Mon Sep 17 00:00:00 2001 From: Riccardo Capraro Date: Sun, 19 Oct 2025 17:11:03 +0200 Subject: [PATCH 1/9] Update crossplane runtime to v2 - Update crossplane-runtime from v1.20.0 to v2.0.0 - Implement missing interface methods for crossplane-runtime v2 compatibility: - Add GetUsers/SetUsers for UserCounter interface on ProviderConfig - Add GetItems for ProviderConfigUsageList interface - Add SetResourceReference/GetResourceReference for ProviderConfigUsage - Add compile-time interface validation with var _ assignments - Fix JQ expression syntax in e2e test examples: - Remove problematic outer parentheses from URL expressions - Fix Authorization header secret injection syntax - Add interface casting tests to prevent runtime panics - Update go.mod and go.sum for crossplane-runtime v2 dependencies - Use chainsaw for e2e tests Signed-off-by: Riccardo Capraro --- Makefile | 4 +- .../v1alpha1/disposablerequest_types.go | 2 +- .../v1alpha1/zz_generated.managed.go | 151 ++++++++-------- .../v1alpha1/zz_generated.managedlist.go | 29 ---- .../v1alpha2/disposablerequest_types.go | 2 +- .../v1alpha2/zz_generated.managed.go | 151 ++++++++-------- .../v1alpha2/zz_generated.managedlist.go | 29 ---- apis/http.go | 8 +- apis/request/v1alpha1/request_types.go | 2 +- apis/request/v1alpha1/zz_generated.managed.go | 151 ++++++++-------- .../v1alpha1/zz_generated.managedlist.go | 29 ---- apis/request/v1alpha2/request_types.go | 2 +- apis/request/v1alpha2/zz_generated.managed.go | 151 ++++++++-------- .../v1alpha2/zz_generated.managedlist.go | 29 ---- apis/v1alpha1/providerconfig_types.go | 27 ++- apis/v1alpha1/providerconfigusage_types.go | 26 ++- apis/v1alpha1/zz_generated.pc.go | 40 ----- apis/v1alpha1/zz_generated.pcu.go | 40 ----- apis/v1alpha1/zz_generated.pculist.go | 29 ---- build | 2 +- cmd/provider/main.go | 8 +- examples/sample/request.yaml | 10 +- go.mod | 2 +- go.sum | 4 +- .../APIVERSION/KIND_LOWER_types.go.tmpl | 2 +- .../controller/KIND_LOWER/KIND_LOWER.go.tmpl | 12 +- .../KIND_LOWER/KIND_LOWER_test.go.tmpl | 6 +- internal/clients/http/client.go | 2 +- internal/controller/config/config.go | 11 +- .../disposablerequest/disposablerequest.go | 24 +-- .../disposablerequest_test.go | 8 +- internal/controller/http.go | 2 +- .../request/observe/is_deleted_check.go | 2 +- .../request/observe/is_deleted_check_test.go | 4 +- .../request/observe/is_synced_check.go | 2 +- .../request/observe/is_synced_check_test.go | 4 +- .../controller/request/observe/jq_check.go | 2 +- .../request/observe/jq_check_test.go | 4 +- internal/controller/request/observe_test.go | 4 +- internal/controller/request/request.go | 24 +-- internal/controller/request/request_test.go | 8 +- .../request/requestgen/request_generator.go | 2 +- .../requestgen/request_generator_test.go | 4 +- .../request/requestmapping/mapping.go | 2 +- .../request/requestmapping/mapping_test.go | 4 +- .../request_processing_test.go | 2 +- .../request/statushandler/status.go | 2 +- .../request/statushandler/status_test.go | 4 +- internal/data-patcher/parser.go | 2 +- internal/data-patcher/parser_test.go | 4 +- internal/data-patcher/patch.go | 2 +- internal/data-patcher/patch_test.go | 4 +- internal/data-patcher/secret_patcher.go | 2 +- internal/data-patcher/secret_patcher_test.go | 2 +- internal/jq/parser_test.go | 2 +- internal/kube-handler/client_test.go | 2 +- internal/utils/set_status_test.go | 2 +- internal/utils/validate_test.go | 2 +- ...http.crossplane.io_disposablerequests.yaml | 162 ------------------ package/crds/http.crossplane.io_requests.yaml | 162 ------------------ 60 files changed, 433 insertions(+), 984 deletions(-) delete mode 100644 apis/disposablerequest/v1alpha1/zz_generated.managedlist.go delete mode 100644 apis/disposablerequest/v1alpha2/zz_generated.managedlist.go delete mode 100644 apis/request/v1alpha1/zz_generated.managedlist.go delete mode 100644 apis/request/v1alpha2/zz_generated.managedlist.go delete mode 100644 apis/v1alpha1/zz_generated.pc.go delete mode 100644 apis/v1alpha1/zz_generated.pcu.go delete mode 100644 apis/v1alpha1/zz_generated.pculist.go diff --git a/Makefile b/Makefile index 66613c7..a8efe73 100644 --- a/Makefile +++ b/Makefile @@ -92,9 +92,9 @@ CROSSPLANE_NAMESPACE = crossplane-system UPTEST_EXAMPLE_LIST := $(shell find ./examples/sample -path '*.yaml' | paste -s -d ',' - ) -uptest: $(UPTEST) $(KUBECTL) $(KUTTL) +uptest: $(UPTEST) $(KUBECTL) $(CHAINSAW) $(CROSSPLANE_CLI) @$(INFO) running automated tests - @KUBECTL=$(KUBECTL) KUTTL=$(KUTTL) CROSSPLANE_NAMESPACE=$(CROSSPLANE_NAMESPACE) $(UPTEST) e2e "$(UPTEST_EXAMPLE_LIST)" --setup-script=cluster/test/setup.sh || $(FAIL) + @KUBECTL=$(KUBECTL) CHAINSAW=$(CHAINSAW) CROSSPLANE_CLI=$(CROSSPLANE_CLI) CROSSPLANE_NAMESPACE=$(CROSSPLANE_NAMESPACE) $(UPTEST) e2e "$(UPTEST_EXAMPLE_LIST)" --setup-script=cluster/test/setup.sh || $(FAIL) @$(OK) running automated tests local-dev: controlplane.up diff --git a/apis/disposablerequest/v1alpha1/disposablerequest_types.go b/apis/disposablerequest/v1alpha1/disposablerequest_types.go index 814275e..9f0edeb 100644 --- a/apis/disposablerequest/v1alpha1/disposablerequest_types.go +++ b/apis/disposablerequest/v1alpha1/disposablerequest_types.go @@ -22,7 +22,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" ) // DisposableRequestParameters are the configurable fields of a DisposableRequest. diff --git a/apis/disposablerequest/v1alpha1/zz_generated.managed.go b/apis/disposablerequest/v1alpha1/zz_generated.managed.go index 1a249e8..1191607 100644 --- a/apis/disposablerequest/v1alpha1/zz_generated.managed.go +++ b/apis/disposablerequest/v1alpha1/zz_generated.managed.go @@ -1,80 +1,71 @@ -/* -Copyright 2020 The Crossplane Authors. - -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. -*/ -// Code generated by angryjet. DO NOT EDIT. - -package v1alpha1 - -import xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" - -// GetCondition of this DisposableRequest. -func (mg *DisposableRequest) GetCondition(ct xpv1.ConditionType) xpv1.Condition { - return mg.Status.GetCondition(ct) -} - -// GetDeletionPolicy of this DisposableRequest. -func (mg *DisposableRequest) GetDeletionPolicy() xpv1.DeletionPolicy { - return mg.Spec.DeletionPolicy -} - -// GetManagementPolicies of this DisposableRequest. -func (mg *DisposableRequest) GetManagementPolicies() xpv1.ManagementPolicies { - return mg.Spec.ManagementPolicies -} - -// GetProviderConfigReference of this DisposableRequest. -func (mg *DisposableRequest) GetProviderConfigReference() *xpv1.Reference { - return mg.Spec.ProviderConfigReference -} - -// GetPublishConnectionDetailsTo of this DisposableRequest. -func (mg *DisposableRequest) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { - return mg.Spec.PublishConnectionDetailsTo -} - -// GetWriteConnectionSecretToReference of this DisposableRequest. -func (mg *DisposableRequest) GetWriteConnectionSecretToReference() *xpv1.SecretReference { - return mg.Spec.WriteConnectionSecretToReference -} - -// SetConditions of this DisposableRequest. -func (mg *DisposableRequest) SetConditions(c ...xpv1.Condition) { - mg.Status.SetConditions(c...) -} - -// SetDeletionPolicy of this DisposableRequest. -func (mg *DisposableRequest) SetDeletionPolicy(r xpv1.DeletionPolicy) { - mg.Spec.DeletionPolicy = r -} - -// SetManagementPolicies of this DisposableRequest. -func (mg *DisposableRequest) SetManagementPolicies(r xpv1.ManagementPolicies) { - mg.Spec.ManagementPolicies = r -} - -// SetProviderConfigReference of this DisposableRequest. -func (mg *DisposableRequest) SetProviderConfigReference(r *xpv1.Reference) { - mg.Spec.ProviderConfigReference = r -} - -// SetPublishConnectionDetailsTo of this DisposableRequest. -func (mg *DisposableRequest) SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo) { - mg.Spec.PublishConnectionDetailsTo = r -} - -// SetWriteConnectionSecretToReference of this DisposableRequest. -func (mg *DisposableRequest) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { - mg.Spec.WriteConnectionSecretToReference = r -} +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ + +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha1 + +import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + +// GetCondition of this DisposableRequest. +func (mg *DisposableRequest) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this DisposableRequest. +func (mg *DisposableRequest) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this DisposableRequest. +func (mg *DisposableRequest) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this DisposableRequest. +func (mg *DisposableRequest) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetWriteConnectionSecretToReference of this DisposableRequest. +func (mg *DisposableRequest) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this DisposableRequest. +func (mg *DisposableRequest) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this DisposableRequest. +func (mg *DisposableRequest) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this DisposableRequest. +func (mg *DisposableRequest) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this DisposableRequest. +func (mg *DisposableRequest) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetWriteConnectionSecretToReference of this DisposableRequest. +func (mg *DisposableRequest) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} \ No newline at end of file diff --git a/apis/disposablerequest/v1alpha1/zz_generated.managedlist.go b/apis/disposablerequest/v1alpha1/zz_generated.managedlist.go deleted file mode 100644 index f2bea17..0000000 --- a/apis/disposablerequest/v1alpha1/zz_generated.managedlist.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2020 The Crossplane Authors. - -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. -*/ -// Code generated by angryjet. DO NOT EDIT. - -package v1alpha1 - -import resource "github.com/crossplane/crossplane-runtime/pkg/resource" - -// GetItems of this DisposableRequestList. -func (l *DisposableRequestList) GetItems() []resource.Managed { - items := make([]resource.Managed, len(l.Items)) - for i := range l.Items { - items[i] = &l.Items[i] - } - return items -} diff --git a/apis/disposablerequest/v1alpha2/disposablerequest_types.go b/apis/disposablerequest/v1alpha2/disposablerequest_types.go index aabbe32..f97cce6 100644 --- a/apis/disposablerequest/v1alpha2/disposablerequest_types.go +++ b/apis/disposablerequest/v1alpha2/disposablerequest_types.go @@ -23,7 +23,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "github.com/crossplane-contrib/provider-http/apis/common" - xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" ) // DisposableRequestParameters are the configurable fields of a DisposableRequest. diff --git a/apis/disposablerequest/v1alpha2/zz_generated.managed.go b/apis/disposablerequest/v1alpha2/zz_generated.managed.go index e77daa5..01185de 100644 --- a/apis/disposablerequest/v1alpha2/zz_generated.managed.go +++ b/apis/disposablerequest/v1alpha2/zz_generated.managed.go @@ -1,80 +1,71 @@ -/* -Copyright 2020 The Crossplane Authors. - -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. -*/ -// Code generated by angryjet. DO NOT EDIT. - -package v1alpha2 - -import xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" - -// GetCondition of this DisposableRequest. -func (mg *DisposableRequest) GetCondition(ct xpv1.ConditionType) xpv1.Condition { - return mg.Status.GetCondition(ct) -} - -// GetDeletionPolicy of this DisposableRequest. -func (mg *DisposableRequest) GetDeletionPolicy() xpv1.DeletionPolicy { - return mg.Spec.DeletionPolicy -} - -// GetManagementPolicies of this DisposableRequest. -func (mg *DisposableRequest) GetManagementPolicies() xpv1.ManagementPolicies { - return mg.Spec.ManagementPolicies -} - -// GetProviderConfigReference of this DisposableRequest. -func (mg *DisposableRequest) GetProviderConfigReference() *xpv1.Reference { - return mg.Spec.ProviderConfigReference -} - -// GetPublishConnectionDetailsTo of this DisposableRequest. -func (mg *DisposableRequest) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { - return mg.Spec.PublishConnectionDetailsTo -} - -// GetWriteConnectionSecretToReference of this DisposableRequest. -func (mg *DisposableRequest) GetWriteConnectionSecretToReference() *xpv1.SecretReference { - return mg.Spec.WriteConnectionSecretToReference -} - -// SetConditions of this DisposableRequest. -func (mg *DisposableRequest) SetConditions(c ...xpv1.Condition) { - mg.Status.SetConditions(c...) -} - -// SetDeletionPolicy of this DisposableRequest. -func (mg *DisposableRequest) SetDeletionPolicy(r xpv1.DeletionPolicy) { - mg.Spec.DeletionPolicy = r -} - -// SetManagementPolicies of this DisposableRequest. -func (mg *DisposableRequest) SetManagementPolicies(r xpv1.ManagementPolicies) { - mg.Spec.ManagementPolicies = r -} - -// SetProviderConfigReference of this DisposableRequest. -func (mg *DisposableRequest) SetProviderConfigReference(r *xpv1.Reference) { - mg.Spec.ProviderConfigReference = r -} - -// SetPublishConnectionDetailsTo of this DisposableRequest. -func (mg *DisposableRequest) SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo) { - mg.Spec.PublishConnectionDetailsTo = r -} - -// SetWriteConnectionSecretToReference of this DisposableRequest. -func (mg *DisposableRequest) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { - mg.Spec.WriteConnectionSecretToReference = r -} +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ + +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha2 + +import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + +// GetCondition of this DisposableRequest. +func (mg *DisposableRequest) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this DisposableRequest. +func (mg *DisposableRequest) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this DisposableRequest. +func (mg *DisposableRequest) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this DisposableRequest. +func (mg *DisposableRequest) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetWriteConnectionSecretToReference of this DisposableRequest. +func (mg *DisposableRequest) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this DisposableRequest. +func (mg *DisposableRequest) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this DisposableRequest. +func (mg *DisposableRequest) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this DisposableRequest. +func (mg *DisposableRequest) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this DisposableRequest. +func (mg *DisposableRequest) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetWriteConnectionSecretToReference of this DisposableRequest. +func (mg *DisposableRequest) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} diff --git a/apis/disposablerequest/v1alpha2/zz_generated.managedlist.go b/apis/disposablerequest/v1alpha2/zz_generated.managedlist.go deleted file mode 100644 index c55775f..0000000 --- a/apis/disposablerequest/v1alpha2/zz_generated.managedlist.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2020 The Crossplane Authors. - -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. -*/ -// Code generated by angryjet. DO NOT EDIT. - -package v1alpha2 - -import resource "github.com/crossplane/crossplane-runtime/pkg/resource" - -// GetItems of this DisposableRequestList. -func (l *DisposableRequestList) GetItems() []resource.Managed { - items := make([]resource.Managed, len(l.Items)) - for i := range l.Items { - items[i] = &l.Items[i] - } - return items -} diff --git a/apis/http.go b/apis/http.go index 3e48468..c535d7c 100644 --- a/apis/http.go +++ b/apis/http.go @@ -20,8 +20,8 @@ package apis import ( "k8s.io/apimachinery/pkg/runtime" - disposablerequestv1alpha1 "github.com/crossplane-contrib/provider-http/apis/disposablerequest/v1alpha2" - requestv1alpha1 "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + disposablerequestv1alpha2 "github.com/crossplane-contrib/provider-http/apis/disposablerequest/v1alpha2" + requestv1alpha2 "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpv1alpha1 "github.com/crossplane-contrib/provider-http/apis/v1alpha1" ) @@ -29,8 +29,8 @@ func init() { // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back AddToSchemes = append(AddToSchemes, httpv1alpha1.SchemeBuilder.AddToScheme, - disposablerequestv1alpha1.SchemeBuilder.AddToScheme, - requestv1alpha1.SchemeBuilder.AddToScheme, + disposablerequestv1alpha2.SchemeBuilder.AddToScheme, + requestv1alpha2.SchemeBuilder.AddToScheme, ) } diff --git a/apis/request/v1alpha1/request_types.go b/apis/request/v1alpha1/request_types.go index dad3fcf..fa73569 100644 --- a/apis/request/v1alpha1/request_types.go +++ b/apis/request/v1alpha1/request_types.go @@ -22,7 +22,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" ) // RequestParameters are the configurable fields of a Request. diff --git a/apis/request/v1alpha1/zz_generated.managed.go b/apis/request/v1alpha1/zz_generated.managed.go index 4096310..08ec898 100644 --- a/apis/request/v1alpha1/zz_generated.managed.go +++ b/apis/request/v1alpha1/zz_generated.managed.go @@ -1,80 +1,71 @@ -/* -Copyright 2020 The Crossplane Authors. - -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. -*/ -// Code generated by angryjet. DO NOT EDIT. - -package v1alpha1 - -import xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" - -// GetCondition of this Request. -func (mg *Request) GetCondition(ct xpv1.ConditionType) xpv1.Condition { - return mg.Status.GetCondition(ct) -} - -// GetDeletionPolicy of this Request. -func (mg *Request) GetDeletionPolicy() xpv1.DeletionPolicy { - return mg.Spec.DeletionPolicy -} - -// GetManagementPolicies of this Request. -func (mg *Request) GetManagementPolicies() xpv1.ManagementPolicies { - return mg.Spec.ManagementPolicies -} - -// GetProviderConfigReference of this Request. -func (mg *Request) GetProviderConfigReference() *xpv1.Reference { - return mg.Spec.ProviderConfigReference -} - -// GetPublishConnectionDetailsTo of this Request. -func (mg *Request) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { - return mg.Spec.PublishConnectionDetailsTo -} - -// GetWriteConnectionSecretToReference of this Request. -func (mg *Request) GetWriteConnectionSecretToReference() *xpv1.SecretReference { - return mg.Spec.WriteConnectionSecretToReference -} - -// SetConditions of this Request. -func (mg *Request) SetConditions(c ...xpv1.Condition) { - mg.Status.SetConditions(c...) -} - -// SetDeletionPolicy of this Request. -func (mg *Request) SetDeletionPolicy(r xpv1.DeletionPolicy) { - mg.Spec.DeletionPolicy = r -} - -// SetManagementPolicies of this Request. -func (mg *Request) SetManagementPolicies(r xpv1.ManagementPolicies) { - mg.Spec.ManagementPolicies = r -} - -// SetProviderConfigReference of this Request. -func (mg *Request) SetProviderConfigReference(r *xpv1.Reference) { - mg.Spec.ProviderConfigReference = r -} - -// SetPublishConnectionDetailsTo of this Request. -func (mg *Request) SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo) { - mg.Spec.PublishConnectionDetailsTo = r -} - -// SetWriteConnectionSecretToReference of this Request. -func (mg *Request) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { - mg.Spec.WriteConnectionSecretToReference = r -} +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ + +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha1 + +import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + +// GetCondition of this Request. +func (mg *Request) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this Request. +func (mg *Request) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this Request. +func (mg *Request) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this Request. +func (mg *Request) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetWriteConnectionSecretToReference of this Request. +func (mg *Request) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this Request. +func (mg *Request) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this Request. +func (mg *Request) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this Request. +func (mg *Request) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this Request. +func (mg *Request) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetWriteConnectionSecretToReference of this Request. +func (mg *Request) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} \ No newline at end of file diff --git a/apis/request/v1alpha1/zz_generated.managedlist.go b/apis/request/v1alpha1/zz_generated.managedlist.go deleted file mode 100644 index badb290..0000000 --- a/apis/request/v1alpha1/zz_generated.managedlist.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2020 The Crossplane Authors. - -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. -*/ -// Code generated by angryjet. DO NOT EDIT. - -package v1alpha1 - -import resource "github.com/crossplane/crossplane-runtime/pkg/resource" - -// GetItems of this RequestList. -func (l *RequestList) GetItems() []resource.Managed { - items := make([]resource.Managed, len(l.Items)) - for i := range l.Items { - items[i] = &l.Items[i] - } - return items -} diff --git a/apis/request/v1alpha2/request_types.go b/apis/request/v1alpha2/request_types.go index a1abe4c..d2258c8 100644 --- a/apis/request/v1alpha2/request_types.go +++ b/apis/request/v1alpha2/request_types.go @@ -23,7 +23,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "github.com/crossplane-contrib/provider-http/apis/common" - xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" ) const ( diff --git a/apis/request/v1alpha2/zz_generated.managed.go b/apis/request/v1alpha2/zz_generated.managed.go index 97a8d05..804c4a4 100644 --- a/apis/request/v1alpha2/zz_generated.managed.go +++ b/apis/request/v1alpha2/zz_generated.managed.go @@ -1,80 +1,71 @@ -/* -Copyright 2020 The Crossplane Authors. - -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. -*/ -// Code generated by angryjet. DO NOT EDIT. - -package v1alpha2 - -import xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" - -// GetCondition of this Request. -func (mg *Request) GetCondition(ct xpv1.ConditionType) xpv1.Condition { - return mg.Status.GetCondition(ct) -} - -// GetDeletionPolicy of this Request. -func (mg *Request) GetDeletionPolicy() xpv1.DeletionPolicy { - return mg.Spec.DeletionPolicy -} - -// GetManagementPolicies of this Request. -func (mg *Request) GetManagementPolicies() xpv1.ManagementPolicies { - return mg.Spec.ManagementPolicies -} - -// GetProviderConfigReference of this Request. -func (mg *Request) GetProviderConfigReference() *xpv1.Reference { - return mg.Spec.ProviderConfigReference -} - -// GetPublishConnectionDetailsTo of this Request. -func (mg *Request) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { - return mg.Spec.PublishConnectionDetailsTo -} - -// GetWriteConnectionSecretToReference of this Request. -func (mg *Request) GetWriteConnectionSecretToReference() *xpv1.SecretReference { - return mg.Spec.WriteConnectionSecretToReference -} - -// SetConditions of this Request. -func (mg *Request) SetConditions(c ...xpv1.Condition) { - mg.Status.SetConditions(c...) -} - -// SetDeletionPolicy of this Request. -func (mg *Request) SetDeletionPolicy(r xpv1.DeletionPolicy) { - mg.Spec.DeletionPolicy = r -} - -// SetManagementPolicies of this Request. -func (mg *Request) SetManagementPolicies(r xpv1.ManagementPolicies) { - mg.Spec.ManagementPolicies = r -} - -// SetProviderConfigReference of this Request. -func (mg *Request) SetProviderConfigReference(r *xpv1.Reference) { - mg.Spec.ProviderConfigReference = r -} - -// SetPublishConnectionDetailsTo of this Request. -func (mg *Request) SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo) { - mg.Spec.PublishConnectionDetailsTo = r -} - -// SetWriteConnectionSecretToReference of this Request. -func (mg *Request) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { - mg.Spec.WriteConnectionSecretToReference = r -} +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ + +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha2 + +import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + +// GetCondition of this Request. +func (mg *Request) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this Request. +func (mg *Request) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this Request. +func (mg *Request) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this Request. +func (mg *Request) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetWriteConnectionSecretToReference of this Request. +func (mg *Request) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this Request. +func (mg *Request) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this Request. +func (mg *Request) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this Request. +func (mg *Request) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this Request. +func (mg *Request) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetWriteConnectionSecretToReference of this Request. +func (mg *Request) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} \ No newline at end of file diff --git a/apis/request/v1alpha2/zz_generated.managedlist.go b/apis/request/v1alpha2/zz_generated.managedlist.go deleted file mode 100644 index 565b460..0000000 --- a/apis/request/v1alpha2/zz_generated.managedlist.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2020 The Crossplane Authors. - -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. -*/ -// Code generated by angryjet. DO NOT EDIT. - -package v1alpha2 - -import resource "github.com/crossplane/crossplane-runtime/pkg/resource" - -// GetItems of this RequestList. -func (l *RequestList) GetItems() []resource.Managed { - items := make([]resource.Managed, len(l.Items)) - for i := range l.Items { - items[i] = &l.Items[i] - } - return items -} diff --git a/apis/v1alpha1/providerconfig_types.go b/apis/v1alpha1/providerconfig_types.go index 2e825d1..31f04ba 100644 --- a/apis/v1alpha1/providerconfig_types.go +++ b/apis/v1alpha1/providerconfig_types.go @@ -22,9 +22,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" ) +// verify casting done in controller +var _ resource.ProviderConfig = &ProviderConfig{} + // A ProviderConfigSpec defines the desired state of a ProviderConfig. type ProviderConfigSpec struct { // Credentials required to authenticate to this provider. @@ -77,6 +81,27 @@ var ( ProviderConfigGroupVersionKind = SchemeGroupVersion.WithKind(ProviderConfigKind) ) +// GetCondition returns the condition for the given ConditionType if exists, +// otherwise returns nil +func (pc *ProviderConfig) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return pc.Status.GetCondition(ct) +} + +// SetConditions sets the conditions on the resource status +func (pc *ProviderConfig) SetConditions(c ...xpv1.Condition) { + pc.Status.SetConditions(c...) +} + +// GetUsers returns the number of users of this ProviderConfig. +func (pc *ProviderConfig) GetUsers() int64 { + return pc.Status.Users +} + +// SetUsers sets the number of users of this ProviderConfig. +func (pc *ProviderConfig) SetUsers(i int64) { + pc.Status.Users = i +} + func init() { SchemeBuilder.Register(&ProviderConfig{}, &ProviderConfigList{}) } diff --git a/apis/v1alpha1/providerconfigusage_types.go b/apis/v1alpha1/providerconfigusage_types.go index ea4dd4a..3c8207b 100644 --- a/apis/v1alpha1/providerconfigusage_types.go +++ b/apis/v1alpha1/providerconfigusage_types.go @@ -22,9 +22,14 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" ) +// verify casting done in controller +var _ resource.ProviderConfigUsage = &ProviderConfigUsage{} +var _ resource.ProviderConfigUsageList = &ProviderConfigUsageList{} + // +kubebuilder:object:root=true // A ProviderConfigUsage indicates that a resource is using a ProviderConfig. @@ -62,6 +67,25 @@ var ( ProviderConfigUsageListGroupVersionKind = SchemeGroupVersion.WithKind(ProviderConfigUsageListKind) ) +// SetResourceReference sets the resource reference. +func (pcu *ProviderConfigUsage) SetResourceReference(r xpv1.TypedReference) { + pcu.ResourceReference = r +} + +// GetResourceReference gets the resource reference. +func (pcu *ProviderConfigUsage) GetResourceReference() xpv1.TypedReference { + return pcu.ResourceReference +} + +// GetItems returns the list of ProviderConfigUsage items. +func (pcul *ProviderConfigUsageList) GetItems() []resource.ProviderConfigUsage { + items := make([]resource.ProviderConfigUsage, len(pcul.Items)) + for i := range pcul.Items { + items[i] = &pcul.Items[i] + } + return items +} + func init() { SchemeBuilder.Register(&ProviderConfigUsage{}, &ProviderConfigUsageList{}) } diff --git a/apis/v1alpha1/zz_generated.pc.go b/apis/v1alpha1/zz_generated.pc.go deleted file mode 100644 index 28fad33..0000000 --- a/apis/v1alpha1/zz_generated.pc.go +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright 2020 The Crossplane Authors. - -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. -*/ -// Code generated by angryjet. DO NOT EDIT. - -package v1alpha1 - -import xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" - -// GetCondition of this ProviderConfig. -func (p *ProviderConfig) GetCondition(ct xpv1.ConditionType) xpv1.Condition { - return p.Status.GetCondition(ct) -} - -// GetUsers of this ProviderConfig. -func (p *ProviderConfig) GetUsers() int64 { - return p.Status.Users -} - -// SetConditions of this ProviderConfig. -func (p *ProviderConfig) SetConditions(c ...xpv1.Condition) { - p.Status.SetConditions(c...) -} - -// SetUsers of this ProviderConfig. -func (p *ProviderConfig) SetUsers(i int64) { - p.Status.Users = i -} diff --git a/apis/v1alpha1/zz_generated.pcu.go b/apis/v1alpha1/zz_generated.pcu.go deleted file mode 100644 index 710c6c6..0000000 --- a/apis/v1alpha1/zz_generated.pcu.go +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright 2020 The Crossplane Authors. - -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. -*/ -// Code generated by angryjet. DO NOT EDIT. - -package v1alpha1 - -import xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" - -// GetProviderConfigReference of this ProviderConfigUsage. -func (p *ProviderConfigUsage) GetProviderConfigReference() xpv1.Reference { - return p.ProviderConfigReference -} - -// GetResourceReference of this ProviderConfigUsage. -func (p *ProviderConfigUsage) GetResourceReference() xpv1.TypedReference { - return p.ResourceReference -} - -// SetProviderConfigReference of this ProviderConfigUsage. -func (p *ProviderConfigUsage) SetProviderConfigReference(r xpv1.Reference) { - p.ProviderConfigReference = r -} - -// SetResourceReference of this ProviderConfigUsage. -func (p *ProviderConfigUsage) SetResourceReference(r xpv1.TypedReference) { - p.ResourceReference = r -} diff --git a/apis/v1alpha1/zz_generated.pculist.go b/apis/v1alpha1/zz_generated.pculist.go deleted file mode 100644 index 468bad0..0000000 --- a/apis/v1alpha1/zz_generated.pculist.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2020 The Crossplane Authors. - -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. -*/ -// Code generated by angryjet. DO NOT EDIT. - -package v1alpha1 - -import resource "github.com/crossplane/crossplane-runtime/pkg/resource" - -// GetItems of this ProviderConfigUsageList. -func (p *ProviderConfigUsageList) GetItems() []resource.ProviderConfigUsage { - items := make([]resource.ProviderConfigUsage, len(p.Items)) - for i := range p.Items { - items[i] = &p.Items[i] - } - return items -} diff --git a/build b/build index 0a8b884..99c79f0 160000 --- a/build +++ b/build @@ -1 +1 @@ -Subproject commit 0a8b8840306079292b7a3746ce1d5382b6868af8 +Subproject commit 99c79f0c310d02157495f457f99b95370200c389 diff --git a/cmd/provider/main.go b/cmd/provider/main.go index 1c47923..2cedde1 100644 --- a/cmd/provider/main.go +++ b/cmd/provider/main.go @@ -28,10 +28,10 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/log/zap" - "github.com/crossplane/crossplane-runtime/pkg/controller" - "github.com/crossplane/crossplane-runtime/pkg/feature" - "github.com/crossplane/crossplane-runtime/pkg/logging" - "github.com/crossplane/crossplane-runtime/pkg/ratelimiter" + "github.com/crossplane/crossplane-runtime/v2/pkg/controller" + "github.com/crossplane/crossplane-runtime/v2/pkg/feature" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/ratelimiter" "github.com/crossplane-contrib/provider-http/apis" template "github.com/crossplane-contrib/provider-http/internal/controller" diff --git a/examples/sample/request.yaml b/examples/sample/request.yaml index 16deb07..4aa7340 100644 --- a/examples/sample/request.yaml +++ b/examples/sample/request.yaml @@ -11,7 +11,7 @@ spec: Content-Type: - application/json Authorization: - - ("Bearer {{ auth:default:token }}") + - "Bearer {{ auth:default:token }}" payload: baseUrl: http://flask-api.default.svc.cluster.local/v1/users body: | @@ -37,14 +37,14 @@ spec: Content-Type: - application/json Authorization: - - ("Bearer {{ auth:default:token }}") + - "Bearer {{ auth:default:token }}" Extra-Header-For-Post: - extra-value # Scenario 2: Action specified, method not specified (defaults to GET for OBSERVE) - action: OBSERVE # method: "GET" - url: (.payload.baseUrl + "/" + (.response.body.id|tostring)) + url: .payload.baseUrl + "/" + (.response.body.id | tostring) # If the ID of the external resource is known in advance, ownership of the resource can be attempted: # url: (.payload.baseUrl + "/1234567890") @@ -56,12 +56,12 @@ spec: email: .payload.body.email, age: .payload.body.age } - url: (.payload.baseUrl + "/" + (.response.body.id|tostring)) + url: .payload.baseUrl + "/" + (.response.body.id | tostring) # Scenario 4: Action specified, method not specified (defaults to DELETE for REMOVE) - action: REMOVE # method: "DELETE" - url: (.payload.baseUrl + "/" + (.response.body.id|tostring)) + url: .payload.baseUrl + "/" + (.response.body.id | tostring) # expectedResponseCheck is optional. If not specified or if the type is "DEFAULT", # the resource is considered up to date if the GET response containes the PUT body. diff --git a/go.mod b/go.mod index c440025..dd9259b 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.0 toolchain go1.24.6 require ( - github.com/crossplane/crossplane-runtime v1.20.0 + github.com/crossplane/crossplane-runtime/v2 v2.0.0 github.com/crossplane/crossplane-tools v0.0.0-20240522174801-1ad3d4c87f21 github.com/google/go-cmp v0.7.0 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index 4f84974..8170a5e 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/crossplane/crossplane-runtime v1.20.0 h1:I54uipRIecqZyms+vz1J/l62yjVQ7HV5w+Nh3RMrUtc= -github.com/crossplane/crossplane-runtime v1.20.0/go.mod h1:lfV1VJenDc9PNVLxDC80YjPoTm+JdSZ13xlS2h37Dvg= +github.com/crossplane/crossplane-runtime/v2 v2.0.0 h1:PK2pTKfshdDZ5IfoiMRiCi0PBnIjqbS0KGXEJgRdrb4= +github.com/crossplane/crossplane-runtime/v2 v2.0.0/go.mod h1:pkd5UzmE8esaZAApevMutR832GjJ1Qgc5Ngr78ByxrI= github.com/crossplane/crossplane-tools v0.0.0-20240522174801-1ad3d4c87f21 h1:8wb7/zCbVPkeX68WbVESWJmSWQE5SZKzz0g9X4FlXRw= github.com/crossplane/crossplane-tools v0.0.0-20240522174801-1ad3d4c87f21/go.mod h1:cN0Y7PFGQMM8mcagXVCbeQoKtipmFWQTPZYyziCPBUI= github.com/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE= diff --git a/hack/helpers/apis/GROUP_LOWER/APIVERSION/KIND_LOWER_types.go.tmpl b/hack/helpers/apis/GROUP_LOWER/APIVERSION/KIND_LOWER_types.go.tmpl index b1fe54d..358cf07 100644 --- a/hack/helpers/apis/GROUP_LOWER/APIVERSION/KIND_LOWER_types.go.tmpl +++ b/hack/helpers/apis/GROUP_LOWER/APIVERSION/KIND_LOWER_types.go.tmpl @@ -22,7 +22,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" ) // {{ .Env.KIND }}Parameters are the configurable fields of a {{ .Env.KIND }}. diff --git a/hack/helpers/controller/KIND_LOWER/KIND_LOWER.go.tmpl b/hack/helpers/controller/KIND_LOWER/KIND_LOWER.go.tmpl index e3758a4..cb33dc2 100644 --- a/hack/helpers/controller/KIND_LOWER/KIND_LOWER.go.tmpl +++ b/hack/helpers/controller/KIND_LOWER/KIND_LOWER.go.tmpl @@ -25,12 +25,12 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/crossplane/crossplane-runtime/pkg/connection" - "github.com/crossplane/crossplane-runtime/pkg/controller" - "github.com/crossplane/crossplane-runtime/pkg/event" - "github.com/crossplane/crossplane-runtime/pkg/ratelimiter" - "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" - "github.com/crossplane/crossplane-runtime/pkg/resource" + "github.com/crossplane/crossplane-runtime/v2/pkg/connection" + "github.com/crossplane/crossplane-runtime/v2/pkg/controller" + "github.com/crossplane/crossplane-runtime/v2/pkg/event" + "github.com/crossplane/crossplane-runtime/v2/pkg/ratelimiter" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" "github.com/crossplane-contrib/provider-{{ .Env.PROVIDER | strings.ToLower }}/apis/{{ .Env.GROUP | strings.ToLower }}/{{ .Env.APIVERSION | strings.ToLower }}" apisv1alpha1 "github.com/crossplane-contrib/provider-{{ .Env.PROVIDER | strings.ToLower }}/apis/v1alpha1" diff --git a/hack/helpers/controller/KIND_LOWER/KIND_LOWER_test.go.tmpl b/hack/helpers/controller/KIND_LOWER/KIND_LOWER_test.go.tmpl index 7eaf81a..ac52bff 100644 --- a/hack/helpers/controller/KIND_LOWER/KIND_LOWER_test.go.tmpl +++ b/hack/helpers/controller/KIND_LOWER/KIND_LOWER_test.go.tmpl @@ -22,9 +22,9 @@ import ( "github.com/google/go-cmp/cmp" - "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" - "github.com/crossplane/crossplane-runtime/pkg/resource" - "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) // Unlike many Kubernetes projects Crossplane does not use third party testing diff --git a/internal/clients/http/client.go b/internal/clients/http/client.go index de1a244..f58eb9b 100644 --- a/internal/clients/http/client.go +++ b/internal/clients/http/client.go @@ -10,7 +10,7 @@ import ( "net/http" "time" - "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" ) const ( diff --git a/internal/controller/config/config.go b/internal/controller/config/config.go index 38ac4da..bf2c067 100644 --- a/internal/controller/config/config.go +++ b/internal/controller/config/config.go @@ -19,11 +19,11 @@ package config import ( "time" - "github.com/crossplane/crossplane-runtime/pkg/controller" - "github.com/crossplane/crossplane-runtime/pkg/event" - "github.com/crossplane/crossplane-runtime/pkg/ratelimiter" - "github.com/crossplane/crossplane-runtime/pkg/reconciler/providerconfig" - "github.com/crossplane/crossplane-runtime/pkg/resource" + "github.com/crossplane/crossplane-runtime/v2/pkg/controller" + "github.com/crossplane/crossplane-runtime/v2/pkg/event" + "github.com/crossplane/crossplane-runtime/v2/pkg/ratelimiter" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/providerconfig" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" ctrl "sigs.k8s.io/controller-runtime" "github.com/crossplane-contrib/provider-http/apis/v1alpha1" @@ -36,6 +36,7 @@ func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error of := resource.ProviderConfigKinds{ Config: v1alpha1.ProviderConfigGroupVersionKind, + Usage: v1alpha1.ProviderConfigUsageGroupVersionKind, UsageList: v1alpha1.ProviderConfigUsageListGroupVersionKind, } diff --git a/internal/controller/disposablerequest/disposablerequest.go b/internal/controller/disposablerequest/disposablerequest.go index d6da60d..dae4dac 100644 --- a/internal/controller/disposablerequest/disposablerequest.go +++ b/internal/controller/disposablerequest/disposablerequest.go @@ -24,19 +24,19 @@ import ( datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" "github.com/crossplane-contrib/provider-http/internal/jq" - "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" json_util "github.com/crossplane-contrib/provider-http/internal/json" - xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" - "github.com/crossplane/crossplane-runtime/pkg/controller" - "github.com/crossplane/crossplane-runtime/pkg/event" - "github.com/crossplane/crossplane-runtime/pkg/ratelimiter" - "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" - "github.com/crossplane/crossplane-runtime/pkg/resource" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + "github.com/crossplane/crossplane-runtime/v2/pkg/controller" + "github.com/crossplane/crossplane-runtime/v2/pkg/event" + "github.com/crossplane/crossplane-runtime/v2/pkg/ratelimiter" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" "github.com/crossplane-contrib/provider-http/apis/disposablerequest/v1alpha2" apisv1alpha1 "github.com/crossplane-contrib/provider-http/apis/v1alpha1" @@ -67,22 +67,19 @@ const ( // Setup adds a controller that reconciles DisposableRequest managed resources. func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error { name := managed.ControllerName(v1alpha2.DisposableRequestGroupKind) - cps := []managed.ConnectionPublisher{managed.NewAPISecretPublisher(mgr.GetClient(), mgr.GetScheme())} r := managed.NewReconciler(mgr, resource.ManagedKind(v1alpha2.DisposableRequestGroupVersionKind), managed.WithExternalConnecter(&connector{ logger: o.Logger, kube: mgr.GetClient(), - usage: resource.NewProviderConfigUsageTracker(mgr.GetClient(), &apisv1alpha1.ProviderConfigUsage{}), newHttpClientFn: httpClient.NewClient, }), managed.WithLogger(o.Logger.WithValues("controller", name)), managed.WithPollInterval(o.PollInterval), WithCustomPollIntervalHook(), managed.WithTimeout(timeout), - managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), - managed.WithConnectionPublishers(cps...)) + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) return ctrl.NewControllerManagedBy(mgr). Named(name). @@ -95,7 +92,6 @@ func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error type connector struct { logger logging.Logger kube client.Client - usage resource.Tracker newHttpClientFn func(log logging.Logger, timeout time.Duration, creds string) (httpClient.Client, error) } @@ -108,10 +104,6 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E l := c.logger.WithValues("disposableRequest", cr.Name) - if err := c.usage.Track(ctx, mg); err != nil { - return nil, errors.Wrap(err, errTrackPCUsage) - } - pc := &apisv1alpha1.ProviderConfig{} n := types.NamespacedName{Name: cr.GetProviderConfigReference().Name} if err := c.kube.Get(ctx, n, pc); err != nil { diff --git a/internal/controller/disposablerequest/disposablerequest_test.go b/internal/controller/disposablerequest/disposablerequest_test.go index efd4431..6856ca4 100644 --- a/internal/controller/disposablerequest/disposablerequest_test.go +++ b/internal/controller/disposablerequest/disposablerequest_test.go @@ -25,15 +25,15 @@ import ( "github.com/crossplane-contrib/provider-http/apis/disposablerequest/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane-contrib/provider-http/internal/utils" - xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" "github.com/google/go-cmp/cmp" "github.com/pkg/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/crossplane/crossplane-runtime/pkg/logging" - "github.com/crossplane/crossplane-runtime/pkg/resource" - "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) // Unlike many Kubernetes projects Crossplane does not use third party testing diff --git a/internal/controller/http.go b/internal/controller/http.go index 19cf6da..2bdd220 100644 --- a/internal/controller/http.go +++ b/internal/controller/http.go @@ -19,7 +19,7 @@ package controller import ( "time" - "github.com/crossplane/crossplane-runtime/pkg/controller" + "github.com/crossplane/crossplane-runtime/v2/pkg/controller" ctrl "sigs.k8s.io/controller-runtime" "github.com/crossplane-contrib/provider-http/internal/controller/config" diff --git a/internal/controller/request/observe/is_deleted_check.go b/internal/controller/request/observe/is_deleted_check.go index b273573..826b07b 100644 --- a/internal/controller/request/observe/is_deleted_check.go +++ b/internal/controller/request/observe/is_deleted_check.go @@ -6,7 +6,7 @@ import ( "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" - "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/pkg/errors" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/internal/controller/request/observe/is_deleted_check_test.go b/internal/controller/request/observe/is_deleted_check_test.go index dd860c2..d85d65e 100644 --- a/internal/controller/request/observe/is_deleted_check_test.go +++ b/internal/controller/request/observe/is_deleted_check_test.go @@ -7,8 +7,8 @@ import ( "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" - "github.com/crossplane/crossplane-runtime/pkg/logging" - "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" "github.com/pkg/errors" ) diff --git a/internal/controller/request/observe/is_synced_check.go b/internal/controller/request/observe/is_synced_check.go index cedce1a..82ad797 100644 --- a/internal/controller/request/observe/is_synced_check.go +++ b/internal/controller/request/observe/is_synced_check.go @@ -13,7 +13,7 @@ import ( datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" "github.com/crossplane-contrib/provider-http/internal/json" "github.com/crossplane-contrib/provider-http/internal/utils" - "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/pkg/errors" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/internal/controller/request/observe/is_synced_check_test.go b/internal/controller/request/observe/is_synced_check_test.go index 4571e15..62c4ff2 100644 --- a/internal/controller/request/observe/is_synced_check_test.go +++ b/internal/controller/request/observe/is_synced_check_test.go @@ -6,8 +6,8 @@ import ( "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" - "github.com/crossplane/crossplane-runtime/pkg/logging" - "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" "github.com/pkg/errors" ) diff --git a/internal/controller/request/observe/jq_check.go b/internal/controller/request/observe/jq_check.go index 44faa23..881e9bf 100644 --- a/internal/controller/request/observe/jq_check.go +++ b/internal/controller/request/observe/jq_check.go @@ -11,7 +11,7 @@ import ( datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" "github.com/crossplane-contrib/provider-http/internal/jq" "github.com/crossplane-contrib/provider-http/internal/utils" - "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/internal/controller/request/observe/jq_check_test.go b/internal/controller/request/observe/jq_check_test.go index e98a4e3..8a25c59 100644 --- a/internal/controller/request/observe/jq_check_test.go +++ b/internal/controller/request/observe/jq_check_test.go @@ -6,8 +6,8 @@ import ( "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" - "github.com/crossplane/crossplane-runtime/pkg/logging" - "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" ) diff --git a/internal/controller/request/observe_test.go b/internal/controller/request/observe_test.go index df2d7c6..a251571 100644 --- a/internal/controller/request/observe_test.go +++ b/internal/controller/request/observe_test.go @@ -10,8 +10,8 @@ import ( "github.com/crossplane-contrib/provider-http/internal/controller/request/observe" "github.com/crossplane-contrib/provider-http/internal/controller/request/requestgen" "github.com/crossplane-contrib/provider-http/internal/controller/request/requestmapping" - "github.com/crossplane/crossplane-runtime/pkg/logging" - "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" "github.com/pkg/errors" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/internal/controller/request/request.go b/internal/controller/request/request.go index 481863c..1ca794b 100644 --- a/internal/controller/request/request.go +++ b/internal/controller/request/request.go @@ -20,18 +20,18 @@ import ( "context" "time" - "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" - "github.com/crossplane/crossplane-runtime/pkg/controller" - "github.com/crossplane/crossplane-runtime/pkg/event" - "github.com/crossplane/crossplane-runtime/pkg/ratelimiter" - "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" - "github.com/crossplane/crossplane-runtime/pkg/resource" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + "github.com/crossplane/crossplane-runtime/v2/pkg/controller" + "github.com/crossplane/crossplane-runtime/v2/pkg/event" + "github.com/crossplane/crossplane-runtime/v2/pkg/ratelimiter" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" apisv1alpha1 "github.com/crossplane-contrib/provider-http/apis/v1alpha1" @@ -61,21 +61,18 @@ const ( // Setup adds a controller that reconciles Request managed resources. func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error { name := managed.ControllerName(v1alpha2.RequestGroupKind) - cps := []managed.ConnectionPublisher{managed.NewAPISecretPublisher(mgr.GetClient(), mgr.GetScheme())} r := managed.NewReconciler(mgr, resource.ManagedKind(v1alpha2.RequestGroupVersionKind), managed.WithExternalConnecter(&connector{ logger: o.Logger, kube: mgr.GetClient(), - usage: resource.NewProviderConfigUsageTracker(mgr.GetClient(), &apisv1alpha1.ProviderConfigUsage{}), newHttpClientFn: httpClient.NewClient, }), managed.WithLogger(o.Logger.WithValues("controller", name)), managed.WithPollInterval(o.PollInterval), managed.WithTimeout(timeout), - managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), - managed.WithConnectionPublishers(cps...)) + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) return ctrl.NewControllerManagedBy(mgr). Named(name). @@ -90,7 +87,6 @@ func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error type connector struct { logger logging.Logger kube client.Client - usage resource.Tracker newHttpClientFn func(log logging.Logger, timeout time.Duration, creds string) (httpClient.Client, error) } @@ -103,10 +99,6 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E l := c.logger.WithValues("request", cr.Name) - if err := c.usage.Track(ctx, mg); err != nil { - return nil, errors.Wrap(err, errTrackPCUsage) - } - pc := &apisv1alpha1.ProviderConfig{} n := types.NamespacedName{Name: cr.GetProviderConfigReference().Name} if err := c.kube.Get(ctx, n, pc); err != nil { diff --git a/internal/controller/request/request_test.go b/internal/controller/request/request_test.go index 064a1d8..dbec6cb 100644 --- a/internal/controller/request/request_test.go +++ b/internal/controller/request/request_test.go @@ -9,10 +9,10 @@ import ( "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" - xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" - "github.com/crossplane/crossplane-runtime/pkg/logging" - "github.com/crossplane/crossplane-runtime/pkg/resource" - "github.com/crossplane/crossplane-runtime/pkg/test" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" "github.com/pkg/errors" ) diff --git a/internal/controller/request/requestgen/request_generator.go b/internal/controller/request/requestgen/request_generator.go index 3a0235d..b33999c 100644 --- a/internal/controller/request/requestgen/request_generator.go +++ b/internal/controller/request/requestgen/request_generator.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/pkg/errors" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/internal/controller/request/requestgen/request_generator_test.go b/internal/controller/request/requestgen/request_generator_test.go index 72532bf..d4dea24 100644 --- a/internal/controller/request/requestgen/request_generator_test.go +++ b/internal/controller/request/requestgen/request_generator_test.go @@ -6,10 +6,10 @@ import ( "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" - "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" ) diff --git a/internal/controller/request/requestmapping/mapping.go b/internal/controller/request/requestmapping/mapping.go index 5a4599f..06a823a 100644 --- a/internal/controller/request/requestmapping/mapping.go +++ b/internal/controller/request/requestmapping/mapping.go @@ -5,7 +5,7 @@ import ( "net/http" "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" - "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/pkg/errors" ) diff --git a/internal/controller/request/requestmapping/mapping_test.go b/internal/controller/request/requestmapping/mapping_test.go index 4ff9a9c..b2033a4 100644 --- a/internal/controller/request/requestmapping/mapping_test.go +++ b/internal/controller/request/requestmapping/mapping_test.go @@ -5,8 +5,8 @@ import ( "testing" "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" - "github.com/crossplane/crossplane-runtime/pkg/logging" - "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" "github.com/pkg/errors" ) diff --git a/internal/controller/request/requestprocessing/request_processing_test.go b/internal/controller/request/requestprocessing/request_processing_test.go index 8d80c81..24fafb5 100644 --- a/internal/controller/request/requestprocessing/request_processing_test.go +++ b/internal/controller/request/requestprocessing/request_processing_test.go @@ -3,7 +3,7 @@ package requestprocessing import ( "testing" - "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" ) diff --git a/internal/controller/request/statushandler/status.go b/internal/controller/request/statushandler/status.go index 98e4aeb..569d628 100644 --- a/internal/controller/request/statushandler/status.go +++ b/internal/controller/request/statushandler/status.go @@ -11,7 +11,7 @@ import ( "github.com/crossplane-contrib/provider-http/internal/controller/request/requestgen" "github.com/crossplane-contrib/provider-http/internal/controller/request/responseconverter" "github.com/crossplane-contrib/provider-http/internal/utils" - "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/internal/controller/request/statushandler/status_test.go b/internal/controller/request/statushandler/status_test.go index 796f012..e0a5dc2 100644 --- a/internal/controller/request/statushandler/status_test.go +++ b/internal/controller/request/statushandler/status_test.go @@ -8,8 +8,8 @@ import ( "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" - "github.com/crossplane/crossplane-runtime/pkg/logging" - "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/internal/data-patcher/parser.go b/internal/data-patcher/parser.go index b8d63dc..79947e5 100644 --- a/internal/data-patcher/parser.go +++ b/internal/data-patcher/parser.go @@ -11,7 +11,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" kubehandler "github.com/crossplane-contrib/provider-http/internal/kube-handler" - "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" ) const ( diff --git a/internal/data-patcher/parser_test.go b/internal/data-patcher/parser_test.go index 14b120c..3405ba7 100644 --- a/internal/data-patcher/parser_test.go +++ b/internal/data-patcher/parser_test.go @@ -5,8 +5,8 @@ import ( "errors" "testing" - "github.com/crossplane/crossplane-runtime/pkg/logging" - "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/internal/data-patcher/patch.go b/internal/data-patcher/patch.go index 2041cb0..b055759 100644 --- a/internal/data-patcher/patch.go +++ b/internal/data-patcher/patch.go @@ -8,7 +8,7 @@ import ( "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" kubehandler "github.com/crossplane-contrib/provider-http/internal/kube-handler" - "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/pkg/errors" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/internal/data-patcher/patch_test.go b/internal/data-patcher/patch_test.go index 113c55f..48f8f0d 100644 --- a/internal/data-patcher/patch_test.go +++ b/internal/data-patcher/patch_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - "github.com/crossplane/crossplane-runtime/pkg/logging" - "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" diff --git a/internal/data-patcher/secret_patcher.go b/internal/data-patcher/secret_patcher.go index a748604..c12707c 100644 --- a/internal/data-patcher/secret_patcher.go +++ b/internal/data-patcher/secret_patcher.go @@ -12,7 +12,7 @@ import ( "github.com/crossplane-contrib/provider-http/internal/jq" json_util "github.com/crossplane-contrib/provider-http/internal/json" kubehandler "github.com/crossplane-contrib/provider-http/internal/kube-handler" - "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/internal/data-patcher/secret_patcher_test.go b/internal/data-patcher/secret_patcher_test.go index f99b3f5..8a02e8f 100644 --- a/internal/data-patcher/secret_patcher_test.go +++ b/internal/data-patcher/secret_patcher_test.go @@ -6,7 +6,7 @@ import ( "github.com/crossplane-contrib/provider-http/apis/common" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" json_util "github.com/crossplane-contrib/provider-http/internal/json" - "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/internal/jq/parser_test.go b/internal/jq/parser_test.go index 427079e..1c31e3a 100644 --- a/internal/jq/parser_test.go +++ b/internal/jq/parser_test.go @@ -3,7 +3,7 @@ package jq import ( "testing" - "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" ) diff --git a/internal/kube-handler/client_test.go b/internal/kube-handler/client_test.go index 8a8b31f..a3ffc84 100644 --- a/internal/kube-handler/client_test.go +++ b/internal/kube-handler/client_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" errorspkg "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" diff --git a/internal/utils/set_status_test.go b/internal/utils/set_status_test.go index e94c247..ea172f4 100644 --- a/internal/utils/set_status_test.go +++ b/internal/utils/set_status_test.go @@ -9,7 +9,7 @@ import ( httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/pkg/errors" - "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" ) diff --git a/internal/utils/validate_test.go b/internal/utils/validate_test.go index e8831a4..444043c 100644 --- a/internal/utils/validate_test.go +++ b/internal/utils/validate_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" "github.com/pkg/errors" ) diff --git a/package/crds/http.crossplane.io_disposablerequests.yaml b/package/crds/http.crossplane.io_disposablerequests.yaml index 4a607bb..a04f107 100644 --- a/package/crds/http.crossplane.io_disposablerequests.yaml +++ b/package/crds/http.crossplane.io_disposablerequests.yaml @@ -185,93 +185,12 @@ spec: required: - name type: object - publishConnectionDetailsTo: - description: |- - PublishConnectionDetailsTo specifies the connection secret config which - contains a name, metadata and a reference to secret store config to - which any connection details for this managed resource should be written. - Connection details frequently include the endpoint, username, - and password required to connect to the managed resource. - properties: - configRef: - default: - name: default - description: |- - SecretStoreConfigRef specifies which secret store config should be used - for this ConnectionSecret. - properties: - name: - description: Name of the referenced object. - type: string - policy: - description: Policies for referencing. - properties: - resolution: - default: Required - description: |- - Resolution specifies whether resolution of this reference is required. - The default is 'Required', which means the reconcile will fail if the - reference cannot be resolved. 'Optional' means this reference will be - a no-op if it cannot be resolved. - enum: - - Required - - Optional - type: string - resolve: - description: |- - Resolve specifies when this reference should be resolved. The default - is 'IfNotPresent', which will attempt to resolve the reference only when - the corresponding field is not present. Use 'Always' to resolve the - reference on every reconcile. - enum: - - Always - - IfNotPresent - type: string - type: object - required: - - name - type: object - metadata: - description: Metadata is the metadata for connection secret. - properties: - annotations: - additionalProperties: - type: string - description: |- - Annotations are the annotations to be added to connection secret. - - For Kubernetes secrets, this will be used as "metadata.annotations". - - It is up to Secret Store implementation for others store types. - type: object - labels: - additionalProperties: - type: string - description: |- - Labels are the labels/tags to be added to connection secret. - - For Kubernetes secrets, this will be used as "metadata.labels". - - It is up to Secret Store implementation for others store types. - type: object - type: - description: |- - Type is the SecretType for the connection secret. - - Only valid for Kubernetes Secret Stores. - type: string - type: object - name: - description: Name is the name of the connection secret. - type: string - required: - - name - type: object writeConnectionSecretToRef: description: |- WriteConnectionSecretToReference specifies the namespace and name of a Secret to which any connection details for this managed resource should be written. Connection details frequently include the endpoint, username, and password required to connect to the managed resource. - This field is planned to be replaced in a future release in favor of - PublishConnectionDetailsTo. Currently, both could be set independently - and connection details would be published to both without affecting - each other. properties: name: description: Name of the secret. @@ -658,93 +577,12 @@ spec: required: - name type: object - publishConnectionDetailsTo: - description: |- - PublishConnectionDetailsTo specifies the connection secret config which - contains a name, metadata and a reference to secret store config to - which any connection details for this managed resource should be written. - Connection details frequently include the endpoint, username, - and password required to connect to the managed resource. - properties: - configRef: - default: - name: default - description: |- - SecretStoreConfigRef specifies which secret store config should be used - for this ConnectionSecret. - properties: - name: - description: Name of the referenced object. - type: string - policy: - description: Policies for referencing. - properties: - resolution: - default: Required - description: |- - Resolution specifies whether resolution of this reference is required. - The default is 'Required', which means the reconcile will fail if the - reference cannot be resolved. 'Optional' means this reference will be - a no-op if it cannot be resolved. - enum: - - Required - - Optional - type: string - resolve: - description: |- - Resolve specifies when this reference should be resolved. The default - is 'IfNotPresent', which will attempt to resolve the reference only when - the corresponding field is not present. Use 'Always' to resolve the - reference on every reconcile. - enum: - - Always - - IfNotPresent - type: string - type: object - required: - - name - type: object - metadata: - description: Metadata is the metadata for connection secret. - properties: - annotations: - additionalProperties: - type: string - description: |- - Annotations are the annotations to be added to connection secret. - - For Kubernetes secrets, this will be used as "metadata.annotations". - - It is up to Secret Store implementation for others store types. - type: object - labels: - additionalProperties: - type: string - description: |- - Labels are the labels/tags to be added to connection secret. - - For Kubernetes secrets, this will be used as "metadata.labels". - - It is up to Secret Store implementation for others store types. - type: object - type: - description: |- - Type is the SecretType for the connection secret. - - Only valid for Kubernetes Secret Stores. - type: string - type: object - name: - description: Name is the name of the connection secret. - type: string - required: - - name - type: object writeConnectionSecretToRef: description: |- WriteConnectionSecretToReference specifies the namespace and name of a Secret to which any connection details for this managed resource should be written. Connection details frequently include the endpoint, username, and password required to connect to the managed resource. - This field is planned to be replaced in a future release in favor of - PublishConnectionDetailsTo. Currently, both could be set independently - and connection details would be published to both without affecting - each other. properties: name: description: Name of the secret. diff --git a/package/crds/http.crossplane.io_requests.yaml b/package/crds/http.crossplane.io_requests.yaml index e505727..c1f4ca6 100644 --- a/package/crds/http.crossplane.io_requests.yaml +++ b/package/crds/http.crossplane.io_requests.yaml @@ -187,93 +187,12 @@ spec: required: - name type: object - publishConnectionDetailsTo: - description: |- - PublishConnectionDetailsTo specifies the connection secret config which - contains a name, metadata and a reference to secret store config to - which any connection details for this managed resource should be written. - Connection details frequently include the endpoint, username, - and password required to connect to the managed resource. - properties: - configRef: - default: - name: default - description: |- - SecretStoreConfigRef specifies which secret store config should be used - for this ConnectionSecret. - properties: - name: - description: Name of the referenced object. - type: string - policy: - description: Policies for referencing. - properties: - resolution: - default: Required - description: |- - Resolution specifies whether resolution of this reference is required. - The default is 'Required', which means the reconcile will fail if the - reference cannot be resolved. 'Optional' means this reference will be - a no-op if it cannot be resolved. - enum: - - Required - - Optional - type: string - resolve: - description: |- - Resolve specifies when this reference should be resolved. The default - is 'IfNotPresent', which will attempt to resolve the reference only when - the corresponding field is not present. Use 'Always' to resolve the - reference on every reconcile. - enum: - - Always - - IfNotPresent - type: string - type: object - required: - - name - type: object - metadata: - description: Metadata is the metadata for connection secret. - properties: - annotations: - additionalProperties: - type: string - description: |- - Annotations are the annotations to be added to connection secret. - - For Kubernetes secrets, this will be used as "metadata.annotations". - - It is up to Secret Store implementation for others store types. - type: object - labels: - additionalProperties: - type: string - description: |- - Labels are the labels/tags to be added to connection secret. - - For Kubernetes secrets, this will be used as "metadata.labels". - - It is up to Secret Store implementation for others store types. - type: object - type: - description: |- - Type is the SecretType for the connection secret. - - Only valid for Kubernetes Secret Stores. - type: string - type: object - name: - description: Name is the name of the connection secret. - type: string - required: - - name - type: object writeConnectionSecretToRef: description: |- WriteConnectionSecretToReference specifies the namespace and name of a Secret to which any connection details for this managed resource should be written. Connection details frequently include the endpoint, username, and password required to connect to the managed resource. - This field is planned to be replaced in a future release in favor of - PublishConnectionDetailsTo. Currently, both could be set independently - and connection details would be published to both without affecting - each other. properties: name: description: Name of the secret. @@ -733,93 +652,12 @@ spec: required: - name type: object - publishConnectionDetailsTo: - description: |- - PublishConnectionDetailsTo specifies the connection secret config which - contains a name, metadata and a reference to secret store config to - which any connection details for this managed resource should be written. - Connection details frequently include the endpoint, username, - and password required to connect to the managed resource. - properties: - configRef: - default: - name: default - description: |- - SecretStoreConfigRef specifies which secret store config should be used - for this ConnectionSecret. - properties: - name: - description: Name of the referenced object. - type: string - policy: - description: Policies for referencing. - properties: - resolution: - default: Required - description: |- - Resolution specifies whether resolution of this reference is required. - The default is 'Required', which means the reconcile will fail if the - reference cannot be resolved. 'Optional' means this reference will be - a no-op if it cannot be resolved. - enum: - - Required - - Optional - type: string - resolve: - description: |- - Resolve specifies when this reference should be resolved. The default - is 'IfNotPresent', which will attempt to resolve the reference only when - the corresponding field is not present. Use 'Always' to resolve the - reference on every reconcile. - enum: - - Always - - IfNotPresent - type: string - type: object - required: - - name - type: object - metadata: - description: Metadata is the metadata for connection secret. - properties: - annotations: - additionalProperties: - type: string - description: |- - Annotations are the annotations to be added to connection secret. - - For Kubernetes secrets, this will be used as "metadata.annotations". - - It is up to Secret Store implementation for others store types. - type: object - labels: - additionalProperties: - type: string - description: |- - Labels are the labels/tags to be added to connection secret. - - For Kubernetes secrets, this will be used as "metadata.labels". - - It is up to Secret Store implementation for others store types. - type: object - type: - description: |- - Type is the SecretType for the connection secret. - - Only valid for Kubernetes Secret Stores. - type: string - type: object - name: - description: Name is the name of the connection secret. - type: string - required: - - name - type: object writeConnectionSecretToRef: description: |- WriteConnectionSecretToReference specifies the namespace and name of a Secret to which any connection details for this managed resource should be written. Connection details frequently include the endpoint, username, and password required to connect to the managed resource. - This field is planned to be replaced in a future release in favor of - PublishConnectionDetailsTo. Currently, both could be set independently - and connection details would be published to both without affecting - each other. properties: name: description: Name of the secret. From 5749cbdc0288e61269e526a32743731bdafbb239 Mon Sep 17 00:00:00 2001 From: Riccardo Capraro Date: Wed, 22 Oct 2025 12:23:10 +0200 Subject: [PATCH 2/9] Add support for namespaced resources Key changes include: - Complete API reorganization with cluster/ and namespaced/ separation - Enhanced controller architecture with dual registration - Comprehensive examples and documentation for both scopes - Updated build configuration and CRD generation All changes are backwards compatible Signed-off-by: Riccardo Capraro --- .github/workflows/ci.yml | 15 + Makefile | 4 +- README.md | 31 +- .../disposablerequest/disposablerequest.go | 0 .../v1alpha1/disposablerequest_types.go | 0 .../disposablerequest/v1alpha1/doc.go | 0 .../v1alpha1/groupversion_info.go | 0 .../v1alpha1/status_setters.go | 0 .../v1alpha1/zz_generated.deepcopy.go | 0 .../v1alpha1/zz_generated.managed.go | 142 ++-- .../v1alpha2/disposablerequest_types.go | 0 .../disposablerequest/v1alpha2/doc.go | 0 .../v1alpha2/groupversion_info.go | 0 .../v1alpha2/status_setters.go | 0 .../v1alpha2/zz_generated.deepcopy.go | 0 .../v1alpha2/zz_generated.managed.go | 142 ++-- apis/{ => cluster}/request/request.go | 0 apis/{ => cluster}/request/v1alpha1/doc.go | 0 .../request/v1alpha1/groupversion_info.go | 0 .../request/v1alpha1/request_types.go | 0 .../request/v1alpha1/status_setters.go | 0 .../request/v1alpha1/zz_generated.deepcopy.go | 0 .../request/v1alpha1/zz_generated.managed.go | 142 ++-- apis/{ => cluster}/request/v1alpha2/doc.go | 0 .../request/v1alpha2/groupversion_info.go | 0 .../request/v1alpha2/request_types.go | 0 .../request/v1alpha2/status_setters.go | 0 .../request/v1alpha2/zz_generated.deepcopy.go | 0 .../request/v1alpha2/zz_generated.managed.go | 142 ++-- apis/{ => cluster}/v1alpha1/doc.go | 0 .../v1alpha1/groupversion_info.go | 0 .../v1alpha1/providerconfig_types.go | 0 .../v1alpha1/providerconfigusage_types.go | 0 .../v1alpha1/zz_generated.deepcopy.go | 0 apis/http.go | 18 +- .../v1alpha2/disposablerequest_types.go | 133 ++++ .../disposablerequest/v1alpha2/doc.go | 21 + .../v1alpha2/groupversion_info.go | 40 + .../v1alpha2/status_setters.go | 44 ++ .../v1alpha2/zz_generated.deepcopy.go | 237 ++++++ .../v1alpha2/zz_generated.managed.go | 71 ++ apis/namespaced/request/v1alpha2/doc.go | 21 + .../request/v1alpha2/groupversion_info.go | 40 + .../request/v1alpha2/request_types.go | 171 +++++ .../request/v1alpha2/status_setters.go | 41 ++ .../request/v1alpha2/zz_generated.deepcopy.go | 283 +++++++ .../request/v1alpha2/zz_generated.managed.go | 71 ++ .../v1alpha2/clusterproviderconfig_types.go | 149 ++++ apis/namespaced/v1alpha2/doc.go | 21 + apis/namespaced/v1alpha2/groupversion_info.go | 40 + .../v1alpha2/providerconfig_types.go | 169 +++++ .../v1alpha2/zz_generated.deepcopy.go | 307 ++++++++ cmd/provider/main.go | 13 +- examples/namespaced/README.md | 91 +++ .../namespaced/clusterproviderconfig.yaml | 7 + .../namespaced/disposablerequest-jwt.yaml | 36 + ...blerequest-with-clusterproviderconfig.yaml | 61 ++ examples/namespaced/disposablerequest.yaml | 58 ++ examples/namespaced/providerconfig.yaml | 8 + .../request-with-clusterproviderconfig.yaml | 118 +++ examples/namespaced/request.yaml | 128 ++++ go.mod | 2 +- internal/controller/cluster/cluster.go | 69 ++ .../controller/{ => cluster}/config/config.go | 2 +- .../disposablerequest/disposablerequest.go | 12 +- .../disposablerequest_test.go | 2 +- .../{ => cluster}/request/observe.go | 8 +- .../request/observe/is_deleted_check.go | 2 +- .../request/observe/is_deleted_check_test.go | 2 +- .../request/observe/is_synced_check.go | 6 +- .../request/observe/is_synced_check_test.go | 2 +- .../{ => cluster}/request/observe/jq_check.go | 6 +- .../request/observe/jq_check_test.go | 2 +- .../{ => cluster}/request/observe_test.go | 8 +- .../{ => cluster}/request/request.go | 20 +- .../{ => cluster}/request/request_test.go | 2 +- .../request/requestgen/request_generator.go | 4 +- .../requestgen/request_generator_test.go | 2 +- .../request/requestmapping/mapping.go | 2 +- .../request/requestmapping/mapping_test.go | 2 +- .../requestprocessing/request_processing.go | 0 .../request_processing_test.go | 0 .../request/responseconverter/converter.go | 2 +- .../responseconverter/converter_test.go | 2 +- .../request/statushandler/status.go | 6 +- .../request/statushandler/status_test.go | 2 +- internal/controller/http.go | 25 +- .../namespaced/config/clusterconfig.go | 53 ++ .../controller/namespaced/config/config.go | 53 ++ .../disposablerequest/disposablerequest.go | 457 ++++++++++++ internal/controller/namespaced/namespaced.go | 77 ++ .../controller/namespaced/request/observe.go | 133 ++++ .../request/observe/is_deleted_check.go | 77 ++ .../request/observe/is_deleted_check_test.go | 243 ++++++ .../request/observe/is_synced_check.go | 166 +++++ .../request/observe/is_synced_check_test.go | 266 +++++++ .../namespaced/request/observe/jq_check.go | 56 ++ .../request/observe/jq_check_test.go | 116 +++ .../controller/namespaced/request/request.go | 250 +++++++ .../request/requestgen/request_generator.go | 156 ++++ .../requestgen/request_generator_test.go | 456 ++++++++++++ .../request/requestmapping/mapping.go | 73 ++ .../request/requestmapping/mapping_test.go | 362 +++++++++ .../requestprocessing/request_processing.go | 32 + .../request_processing_test.go | 149 ++++ .../request/responseconverter/converter.go | 15 + .../responseconverter/converter_test.go | 55 ++ .../request/statushandler/status.go | 152 ++++ .../request/statushandler/status_test.go | 253 +++++++ internal/controller/typeconv/conversion.go | 238 ++++++ .../controller/typeconv/conversion_test.go | 691 ++++++++++++++++++ internal/data-patcher/patch.go | 2 +- internal/json/util_test.go | 2 +- internal/utils/set_status_test.go | 4 +- ....crossplane.io_clusterproviderconfigs.yaml | 170 +++++ ...splane.io_clusterproviderconfigusages.yaml | 118 +++ ...tp.m.crossplane.io_disposablerequests.yaml | 417 +++++++++++ .../http.m.crossplane.io_providerconfigs.yaml | 169 +++++ ....m.crossplane.io_providerconfigusages.yaml | 117 +++ .../crds/http.m.crossplane.io_requests.yaml | 499 +++++++++++++ package/crossplane.yaml | 3 + 121 files changed, 8942 insertions(+), 345 deletions(-) rename apis/{ => cluster}/disposablerequest/disposablerequest.go (100%) rename apis/{ => cluster}/disposablerequest/v1alpha1/disposablerequest_types.go (100%) rename apis/{ => cluster}/disposablerequest/v1alpha1/doc.go (100%) rename apis/{ => cluster}/disposablerequest/v1alpha1/groupversion_info.go (100%) rename apis/{ => cluster}/disposablerequest/v1alpha1/status_setters.go (100%) rename apis/{ => cluster}/disposablerequest/v1alpha1/zz_generated.deepcopy.go (100%) rename apis/{ => cluster}/disposablerequest/v1alpha1/zz_generated.managed.go (97%) rename apis/{ => cluster}/disposablerequest/v1alpha2/disposablerequest_types.go (100%) rename apis/{ => cluster}/disposablerequest/v1alpha2/doc.go (100%) rename apis/{ => cluster}/disposablerequest/v1alpha2/groupversion_info.go (100%) rename apis/{ => cluster}/disposablerequest/v1alpha2/status_setters.go (100%) rename apis/{ => cluster}/disposablerequest/v1alpha2/zz_generated.deepcopy.go (100%) rename apis/{ => cluster}/disposablerequest/v1alpha2/zz_generated.managed.go (97%) rename apis/{ => cluster}/request/request.go (100%) rename apis/{ => cluster}/request/v1alpha1/doc.go (100%) rename apis/{ => cluster}/request/v1alpha1/groupversion_info.go (100%) rename apis/{ => cluster}/request/v1alpha1/request_types.go (100%) rename apis/{ => cluster}/request/v1alpha1/status_setters.go (100%) rename apis/{ => cluster}/request/v1alpha1/zz_generated.deepcopy.go (100%) rename apis/{ => cluster}/request/v1alpha1/zz_generated.managed.go (96%) rename apis/{ => cluster}/request/v1alpha2/doc.go (100%) rename apis/{ => cluster}/request/v1alpha2/groupversion_info.go (100%) rename apis/{ => cluster}/request/v1alpha2/request_types.go (100%) rename apis/{ => cluster}/request/v1alpha2/status_setters.go (100%) rename apis/{ => cluster}/request/v1alpha2/zz_generated.deepcopy.go (100%) rename apis/{ => cluster}/request/v1alpha2/zz_generated.managed.go (96%) rename apis/{ => cluster}/v1alpha1/doc.go (100%) rename apis/{ => cluster}/v1alpha1/groupversion_info.go (100%) rename apis/{ => cluster}/v1alpha1/providerconfig_types.go (100%) rename apis/{ => cluster}/v1alpha1/providerconfigusage_types.go (100%) rename apis/{ => cluster}/v1alpha1/zz_generated.deepcopy.go (100%) create mode 100644 apis/namespaced/disposablerequest/v1alpha2/disposablerequest_types.go create mode 100644 apis/namespaced/disposablerequest/v1alpha2/doc.go create mode 100644 apis/namespaced/disposablerequest/v1alpha2/groupversion_info.go create mode 100644 apis/namespaced/disposablerequest/v1alpha2/status_setters.go create mode 100644 apis/namespaced/disposablerequest/v1alpha2/zz_generated.deepcopy.go create mode 100644 apis/namespaced/disposablerequest/v1alpha2/zz_generated.managed.go create mode 100644 apis/namespaced/request/v1alpha2/doc.go create mode 100644 apis/namespaced/request/v1alpha2/groupversion_info.go create mode 100644 apis/namespaced/request/v1alpha2/request_types.go create mode 100644 apis/namespaced/request/v1alpha2/status_setters.go create mode 100644 apis/namespaced/request/v1alpha2/zz_generated.deepcopy.go create mode 100644 apis/namespaced/request/v1alpha2/zz_generated.managed.go create mode 100644 apis/namespaced/v1alpha2/clusterproviderconfig_types.go create mode 100644 apis/namespaced/v1alpha2/doc.go create mode 100644 apis/namespaced/v1alpha2/groupversion_info.go create mode 100644 apis/namespaced/v1alpha2/providerconfig_types.go create mode 100644 apis/namespaced/v1alpha2/zz_generated.deepcopy.go create mode 100644 examples/namespaced/README.md create mode 100644 examples/namespaced/clusterproviderconfig.yaml create mode 100644 examples/namespaced/disposablerequest-jwt.yaml create mode 100644 examples/namespaced/disposablerequest-with-clusterproviderconfig.yaml create mode 100644 examples/namespaced/disposablerequest.yaml create mode 100644 examples/namespaced/providerconfig.yaml create mode 100644 examples/namespaced/request-with-clusterproviderconfig.yaml create mode 100644 examples/namespaced/request.yaml create mode 100644 internal/controller/cluster/cluster.go rename internal/controller/{ => cluster}/config/config.go (96%) rename internal/controller/{ => cluster}/disposablerequest/disposablerequest.go (98%) rename internal/controller/{ => cluster}/disposablerequest/disposablerequest_test.go (99%) rename internal/controller/{ => cluster}/request/observe.go (96%) rename internal/controller/{ => cluster}/request/observe/is_deleted_check.go (97%) rename internal/controller/{ => cluster}/request/observe/is_deleted_check_test.go (98%) rename internal/controller/{ => cluster}/request/observe/is_synced_check.go (97%) rename internal/controller/{ => cluster}/request/observe/is_synced_check_test.go (98%) rename internal/controller/{ => cluster}/request/observe/jq_check.go (93%) rename internal/controller/{ => cluster}/request/observe/jq_check_test.go (97%) rename internal/controller/{ => cluster}/request/observe_test.go (99%) rename internal/controller/{ => cluster}/request/request.go (94%) rename internal/controller/{ => cluster}/request/request_test.go (99%) rename internal/controller/{ => cluster}/request/requestgen/request_generator.go (98%) rename internal/controller/{ => cluster}/request/requestgen/request_generator_test.go (99%) rename internal/controller/{ => cluster}/request/requestmapping/mapping.go (96%) rename internal/controller/{ => cluster}/request/requestmapping/mapping_test.go (99%) rename internal/controller/{ => cluster}/request/requestprocessing/request_processing.go (100%) rename internal/controller/{ => cluster}/request/requestprocessing/request_processing_test.go (100%) rename internal/controller/{ => cluster}/request/responseconverter/converter.go (83%) rename internal/controller/{ => cluster}/request/responseconverter/converter_test.go (94%) rename internal/controller/{ => cluster}/request/statushandler/status.go (97%) rename internal/controller/{ => cluster}/request/statushandler/status_test.go (98%) create mode 100644 internal/controller/namespaced/config/clusterconfig.go create mode 100644 internal/controller/namespaced/config/config.go create mode 100644 internal/controller/namespaced/disposablerequest/disposablerequest.go create mode 100644 internal/controller/namespaced/namespaced.go create mode 100644 internal/controller/namespaced/request/observe.go create mode 100644 internal/controller/namespaced/request/observe/is_deleted_check.go create mode 100644 internal/controller/namespaced/request/observe/is_deleted_check_test.go create mode 100644 internal/controller/namespaced/request/observe/is_synced_check.go create mode 100644 internal/controller/namespaced/request/observe/is_synced_check_test.go create mode 100644 internal/controller/namespaced/request/observe/jq_check.go create mode 100644 internal/controller/namespaced/request/observe/jq_check_test.go create mode 100644 internal/controller/namespaced/request/request.go create mode 100644 internal/controller/namespaced/request/requestgen/request_generator.go create mode 100644 internal/controller/namespaced/request/requestgen/request_generator_test.go create mode 100644 internal/controller/namespaced/request/requestmapping/mapping.go create mode 100644 internal/controller/namespaced/request/requestmapping/mapping_test.go create mode 100644 internal/controller/namespaced/request/requestprocessing/request_processing.go create mode 100644 internal/controller/namespaced/request/requestprocessing/request_processing_test.go create mode 100644 internal/controller/namespaced/request/responseconverter/converter.go create mode 100644 internal/controller/namespaced/request/responseconverter/converter_test.go create mode 100644 internal/controller/namespaced/request/statushandler/status.go create mode 100644 internal/controller/namespaced/request/statushandler/status_test.go create mode 100644 internal/controller/typeconv/conversion.go create mode 100644 internal/controller/typeconv/conversion_test.go create mode 100644 package/crds/http.m.crossplane.io_clusterproviderconfigs.yaml create mode 100644 package/crds/http.m.crossplane.io_clusterproviderconfigusages.yaml create mode 100644 package/crds/http.m.crossplane.io_disposablerequests.yaml create mode 100644 package/crds/http.m.crossplane.io_providerconfigs.yaml create mode 100644 package/crds/http.m.crossplane.io_providerconfigusages.yaml create mode 100644 package/crds/http.m.crossplane.io_requests.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f9cee4..88ced83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,6 +111,18 @@ jobs: runs-on: ubuntu-latest needs: detect-noop if: needs.detect-noop.outputs.noop != 'true' + strategy: + fail-fast: false + matrix: + crossplane-version: + - name: "crossplane-v2" + version: "2.0.2" + cli-version: "v2.0.2" + - name: "crossplane-v1" + version: "1.20.1" + cli-version: "v1.20.1" + + name: e2e-tests-${{ matrix.crossplane-version.name }} steps: - name: Setup QEMU @@ -146,3 +158,6 @@ jobs: # We're using docker buildx, which doesn't actually load the images it # builds by default. Specifying --load does so. BUILD_ARGS: "--load" + # Set Crossplane version for this matrix run + CROSSPLANE_VERSION: ${{ matrix.crossplane-version.version }} + CROSSPLANE_CLI_VERSION: ${{ matrix.crossplane-version.cli-version }} diff --git a/Makefile b/Makefile index a8efe73..91c880b 100644 --- a/Makefile +++ b/Makefile @@ -39,8 +39,8 @@ GOLANGCILINT_VERSION = 2.1.2 # ==================================================================================== # Setup Kubernetes tools USE_HELM3 = true -CROSSPLANE_VERSION = 2.0.2 -CROSSPLANE_CLI_VERSION = v2.0.2 +CROSSPLANE_VERSION ?= 2.0.2 +CROSSPLANE_CLI_VERSION ?= v2.0.2 -include build/makelib/k8s_tools.mk diff --git a/README.md b/README.md index 54c1314..9ff6d5c 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,22 @@ To install `provider-http`, you have two options: ## Supported Resources -`provider-http` supports the following resources: +`provider-http` supports resources in two scopes: +### Cluster-scoped Resources (`http.crossplane.io`) - **DisposableRequest:** Initiates a one-time HTTP request. See [DisposableRequest CRD documentation](resources-docs/disposablerequest_docs.md). - **Request:** Manages a resource through HTTP requests. See [Request CRD documentation](resources-docs/request_docs.md). +### Namespaced Resources (`http.m.crossplane.io`) +- **DisposableRequest:** Namespace-scoped version of the disposable HTTP request. +- **Request:** Namespace-scoped version of the managed HTTP resource. +- **ProviderConfig:** Namespace-scoped provider configuration. +- **ClusterProviderConfig:** Cluster-scoped provider configuration for cross-namespace access. + +**When to use each:** +- Use **cluster-scoped** resources for shared infrastructure and when you have cluster-admin privileges +- Use **namespaced** resources for tenant isolation, application-specific resources, and when working with namespace-level permissions + ## Usage ### DisposableRequest @@ -62,6 +73,24 @@ spec: For more detailed examples and configuration options, refer to the [examples directory](examples/sample/). +### Namespaced Resources + +For namespace-scoped resources, use the `http.m.crossplane.io` API group: + +```yaml +apiVersion: http.m.crossplane.io/v1alpha2 +kind: Request +metadata: + name: example-namespaced-request + namespace: my-namespace +spec: + # Add your Request specification here + providerConfigRef: + name: my-namespaced-config +``` + +For namespaced examples and configuration options, refer to the [namespaced examples directory](examples/namespaced/). + ## Developing locally Run controller against the cluster: diff --git a/apis/disposablerequest/disposablerequest.go b/apis/cluster/disposablerequest/disposablerequest.go similarity index 100% rename from apis/disposablerequest/disposablerequest.go rename to apis/cluster/disposablerequest/disposablerequest.go diff --git a/apis/disposablerequest/v1alpha1/disposablerequest_types.go b/apis/cluster/disposablerequest/v1alpha1/disposablerequest_types.go similarity index 100% rename from apis/disposablerequest/v1alpha1/disposablerequest_types.go rename to apis/cluster/disposablerequest/v1alpha1/disposablerequest_types.go diff --git a/apis/disposablerequest/v1alpha1/doc.go b/apis/cluster/disposablerequest/v1alpha1/doc.go similarity index 100% rename from apis/disposablerequest/v1alpha1/doc.go rename to apis/cluster/disposablerequest/v1alpha1/doc.go diff --git a/apis/disposablerequest/v1alpha1/groupversion_info.go b/apis/cluster/disposablerequest/v1alpha1/groupversion_info.go similarity index 100% rename from apis/disposablerequest/v1alpha1/groupversion_info.go rename to apis/cluster/disposablerequest/v1alpha1/groupversion_info.go diff --git a/apis/disposablerequest/v1alpha1/status_setters.go b/apis/cluster/disposablerequest/v1alpha1/status_setters.go similarity index 100% rename from apis/disposablerequest/v1alpha1/status_setters.go rename to apis/cluster/disposablerequest/v1alpha1/status_setters.go diff --git a/apis/disposablerequest/v1alpha1/zz_generated.deepcopy.go b/apis/cluster/disposablerequest/v1alpha1/zz_generated.deepcopy.go similarity index 100% rename from apis/disposablerequest/v1alpha1/zz_generated.deepcopy.go rename to apis/cluster/disposablerequest/v1alpha1/zz_generated.deepcopy.go diff --git a/apis/disposablerequest/v1alpha1/zz_generated.managed.go b/apis/cluster/disposablerequest/v1alpha1/zz_generated.managed.go similarity index 97% rename from apis/disposablerequest/v1alpha1/zz_generated.managed.go rename to apis/cluster/disposablerequest/v1alpha1/zz_generated.managed.go index 1191607..f533d43 100644 --- a/apis/disposablerequest/v1alpha1/zz_generated.managed.go +++ b/apis/cluster/disposablerequest/v1alpha1/zz_generated.managed.go @@ -1,71 +1,71 @@ -/* -Copyright 2020 The Crossplane Authors. - -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. -*/ - -// Code generated by angryjet. DO NOT EDIT. - -package v1alpha1 - -import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" - -// GetCondition of this DisposableRequest. -func (mg *DisposableRequest) GetCondition(ct xpv1.ConditionType) xpv1.Condition { - return mg.Status.GetCondition(ct) -} - -// GetDeletionPolicy of this DisposableRequest. -func (mg *DisposableRequest) GetDeletionPolicy() xpv1.DeletionPolicy { - return mg.Spec.DeletionPolicy -} - -// GetManagementPolicies of this DisposableRequest. -func (mg *DisposableRequest) GetManagementPolicies() xpv1.ManagementPolicies { - return mg.Spec.ManagementPolicies -} - -// GetProviderConfigReference of this DisposableRequest. -func (mg *DisposableRequest) GetProviderConfigReference() *xpv1.Reference { - return mg.Spec.ProviderConfigReference -} - -// GetWriteConnectionSecretToReference of this DisposableRequest. -func (mg *DisposableRequest) GetWriteConnectionSecretToReference() *xpv1.SecretReference { - return mg.Spec.WriteConnectionSecretToReference -} - -// SetConditions of this DisposableRequest. -func (mg *DisposableRequest) SetConditions(c ...xpv1.Condition) { - mg.Status.SetConditions(c...) -} - -// SetDeletionPolicy of this DisposableRequest. -func (mg *DisposableRequest) SetDeletionPolicy(r xpv1.DeletionPolicy) { - mg.Spec.DeletionPolicy = r -} - -// SetManagementPolicies of this DisposableRequest. -func (mg *DisposableRequest) SetManagementPolicies(r xpv1.ManagementPolicies) { - mg.Spec.ManagementPolicies = r -} - -// SetProviderConfigReference of this DisposableRequest. -func (mg *DisposableRequest) SetProviderConfigReference(r *xpv1.Reference) { - mg.Spec.ProviderConfigReference = r -} - -// SetWriteConnectionSecretToReference of this DisposableRequest. -func (mg *DisposableRequest) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { - mg.Spec.WriteConnectionSecretToReference = r -} \ No newline at end of file +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ + +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha1 + +import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + +// GetCondition of this DisposableRequest. +func (mg *DisposableRequest) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this DisposableRequest. +func (mg *DisposableRequest) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this DisposableRequest. +func (mg *DisposableRequest) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this DisposableRequest. +func (mg *DisposableRequest) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetWriteConnectionSecretToReference of this DisposableRequest. +func (mg *DisposableRequest) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this DisposableRequest. +func (mg *DisposableRequest) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this DisposableRequest. +func (mg *DisposableRequest) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this DisposableRequest. +func (mg *DisposableRequest) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this DisposableRequest. +func (mg *DisposableRequest) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetWriteConnectionSecretToReference of this DisposableRequest. +func (mg *DisposableRequest) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} diff --git a/apis/disposablerequest/v1alpha2/disposablerequest_types.go b/apis/cluster/disposablerequest/v1alpha2/disposablerequest_types.go similarity index 100% rename from apis/disposablerequest/v1alpha2/disposablerequest_types.go rename to apis/cluster/disposablerequest/v1alpha2/disposablerequest_types.go diff --git a/apis/disposablerequest/v1alpha2/doc.go b/apis/cluster/disposablerequest/v1alpha2/doc.go similarity index 100% rename from apis/disposablerequest/v1alpha2/doc.go rename to apis/cluster/disposablerequest/v1alpha2/doc.go diff --git a/apis/disposablerequest/v1alpha2/groupversion_info.go b/apis/cluster/disposablerequest/v1alpha2/groupversion_info.go similarity index 100% rename from apis/disposablerequest/v1alpha2/groupversion_info.go rename to apis/cluster/disposablerequest/v1alpha2/groupversion_info.go diff --git a/apis/disposablerequest/v1alpha2/status_setters.go b/apis/cluster/disposablerequest/v1alpha2/status_setters.go similarity index 100% rename from apis/disposablerequest/v1alpha2/status_setters.go rename to apis/cluster/disposablerequest/v1alpha2/status_setters.go diff --git a/apis/disposablerequest/v1alpha2/zz_generated.deepcopy.go b/apis/cluster/disposablerequest/v1alpha2/zz_generated.deepcopy.go similarity index 100% rename from apis/disposablerequest/v1alpha2/zz_generated.deepcopy.go rename to apis/cluster/disposablerequest/v1alpha2/zz_generated.deepcopy.go diff --git a/apis/disposablerequest/v1alpha2/zz_generated.managed.go b/apis/cluster/disposablerequest/v1alpha2/zz_generated.managed.go similarity index 97% rename from apis/disposablerequest/v1alpha2/zz_generated.managed.go rename to apis/cluster/disposablerequest/v1alpha2/zz_generated.managed.go index 01185de..08ed963 100644 --- a/apis/disposablerequest/v1alpha2/zz_generated.managed.go +++ b/apis/cluster/disposablerequest/v1alpha2/zz_generated.managed.go @@ -1,71 +1,71 @@ -/* -Copyright 2020 The Crossplane Authors. - -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. -*/ - -// Code generated by angryjet. DO NOT EDIT. - -package v1alpha2 - -import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" - -// GetCondition of this DisposableRequest. -func (mg *DisposableRequest) GetCondition(ct xpv1.ConditionType) xpv1.Condition { - return mg.Status.GetCondition(ct) -} - -// GetDeletionPolicy of this DisposableRequest. -func (mg *DisposableRequest) GetDeletionPolicy() xpv1.DeletionPolicy { - return mg.Spec.DeletionPolicy -} - -// GetManagementPolicies of this DisposableRequest. -func (mg *DisposableRequest) GetManagementPolicies() xpv1.ManagementPolicies { - return mg.Spec.ManagementPolicies -} - -// GetProviderConfigReference of this DisposableRequest. -func (mg *DisposableRequest) GetProviderConfigReference() *xpv1.Reference { - return mg.Spec.ProviderConfigReference -} - -// GetWriteConnectionSecretToReference of this DisposableRequest. -func (mg *DisposableRequest) GetWriteConnectionSecretToReference() *xpv1.SecretReference { - return mg.Spec.WriteConnectionSecretToReference -} - -// SetConditions of this DisposableRequest. -func (mg *DisposableRequest) SetConditions(c ...xpv1.Condition) { - mg.Status.SetConditions(c...) -} - -// SetDeletionPolicy of this DisposableRequest. -func (mg *DisposableRequest) SetDeletionPolicy(r xpv1.DeletionPolicy) { - mg.Spec.DeletionPolicy = r -} - -// SetManagementPolicies of this DisposableRequest. -func (mg *DisposableRequest) SetManagementPolicies(r xpv1.ManagementPolicies) { - mg.Spec.ManagementPolicies = r -} - -// SetProviderConfigReference of this DisposableRequest. -func (mg *DisposableRequest) SetProviderConfigReference(r *xpv1.Reference) { - mg.Spec.ProviderConfigReference = r -} - -// SetWriteConnectionSecretToReference of this DisposableRequest. -func (mg *DisposableRequest) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { - mg.Spec.WriteConnectionSecretToReference = r -} +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ + +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha2 + +import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + +// GetCondition of this DisposableRequest. +func (mg *DisposableRequest) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this DisposableRequest. +func (mg *DisposableRequest) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this DisposableRequest. +func (mg *DisposableRequest) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this DisposableRequest. +func (mg *DisposableRequest) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetWriteConnectionSecretToReference of this DisposableRequest. +func (mg *DisposableRequest) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this DisposableRequest. +func (mg *DisposableRequest) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this DisposableRequest. +func (mg *DisposableRequest) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this DisposableRequest. +func (mg *DisposableRequest) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this DisposableRequest. +func (mg *DisposableRequest) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetWriteConnectionSecretToReference of this DisposableRequest. +func (mg *DisposableRequest) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} diff --git a/apis/request/request.go b/apis/cluster/request/request.go similarity index 100% rename from apis/request/request.go rename to apis/cluster/request/request.go diff --git a/apis/request/v1alpha1/doc.go b/apis/cluster/request/v1alpha1/doc.go similarity index 100% rename from apis/request/v1alpha1/doc.go rename to apis/cluster/request/v1alpha1/doc.go diff --git a/apis/request/v1alpha1/groupversion_info.go b/apis/cluster/request/v1alpha1/groupversion_info.go similarity index 100% rename from apis/request/v1alpha1/groupversion_info.go rename to apis/cluster/request/v1alpha1/groupversion_info.go diff --git a/apis/request/v1alpha1/request_types.go b/apis/cluster/request/v1alpha1/request_types.go similarity index 100% rename from apis/request/v1alpha1/request_types.go rename to apis/cluster/request/v1alpha1/request_types.go diff --git a/apis/request/v1alpha1/status_setters.go b/apis/cluster/request/v1alpha1/status_setters.go similarity index 100% rename from apis/request/v1alpha1/status_setters.go rename to apis/cluster/request/v1alpha1/status_setters.go diff --git a/apis/request/v1alpha1/zz_generated.deepcopy.go b/apis/cluster/request/v1alpha1/zz_generated.deepcopy.go similarity index 100% rename from apis/request/v1alpha1/zz_generated.deepcopy.go rename to apis/cluster/request/v1alpha1/zz_generated.deepcopy.go diff --git a/apis/request/v1alpha1/zz_generated.managed.go b/apis/cluster/request/v1alpha1/zz_generated.managed.go similarity index 96% rename from apis/request/v1alpha1/zz_generated.managed.go rename to apis/cluster/request/v1alpha1/zz_generated.managed.go index 08ec898..040f4db 100644 --- a/apis/request/v1alpha1/zz_generated.managed.go +++ b/apis/cluster/request/v1alpha1/zz_generated.managed.go @@ -1,71 +1,71 @@ -/* -Copyright 2020 The Crossplane Authors. - -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. -*/ - -// Code generated by angryjet. DO NOT EDIT. - -package v1alpha1 - -import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" - -// GetCondition of this Request. -func (mg *Request) GetCondition(ct xpv1.ConditionType) xpv1.Condition { - return mg.Status.GetCondition(ct) -} - -// GetDeletionPolicy of this Request. -func (mg *Request) GetDeletionPolicy() xpv1.DeletionPolicy { - return mg.Spec.DeletionPolicy -} - -// GetManagementPolicies of this Request. -func (mg *Request) GetManagementPolicies() xpv1.ManagementPolicies { - return mg.Spec.ManagementPolicies -} - -// GetProviderConfigReference of this Request. -func (mg *Request) GetProviderConfigReference() *xpv1.Reference { - return mg.Spec.ProviderConfigReference -} - -// GetWriteConnectionSecretToReference of this Request. -func (mg *Request) GetWriteConnectionSecretToReference() *xpv1.SecretReference { - return mg.Spec.WriteConnectionSecretToReference -} - -// SetConditions of this Request. -func (mg *Request) SetConditions(c ...xpv1.Condition) { - mg.Status.SetConditions(c...) -} - -// SetDeletionPolicy of this Request. -func (mg *Request) SetDeletionPolicy(r xpv1.DeletionPolicy) { - mg.Spec.DeletionPolicy = r -} - -// SetManagementPolicies of this Request. -func (mg *Request) SetManagementPolicies(r xpv1.ManagementPolicies) { - mg.Spec.ManagementPolicies = r -} - -// SetProviderConfigReference of this Request. -func (mg *Request) SetProviderConfigReference(r *xpv1.Reference) { - mg.Spec.ProviderConfigReference = r -} - -// SetWriteConnectionSecretToReference of this Request. -func (mg *Request) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { - mg.Spec.WriteConnectionSecretToReference = r -} \ No newline at end of file +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ + +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha1 + +import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + +// GetCondition of this Request. +func (mg *Request) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this Request. +func (mg *Request) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this Request. +func (mg *Request) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this Request. +func (mg *Request) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetWriteConnectionSecretToReference of this Request. +func (mg *Request) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this Request. +func (mg *Request) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this Request. +func (mg *Request) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this Request. +func (mg *Request) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this Request. +func (mg *Request) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetWriteConnectionSecretToReference of this Request. +func (mg *Request) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} diff --git a/apis/request/v1alpha2/doc.go b/apis/cluster/request/v1alpha2/doc.go similarity index 100% rename from apis/request/v1alpha2/doc.go rename to apis/cluster/request/v1alpha2/doc.go diff --git a/apis/request/v1alpha2/groupversion_info.go b/apis/cluster/request/v1alpha2/groupversion_info.go similarity index 100% rename from apis/request/v1alpha2/groupversion_info.go rename to apis/cluster/request/v1alpha2/groupversion_info.go diff --git a/apis/request/v1alpha2/request_types.go b/apis/cluster/request/v1alpha2/request_types.go similarity index 100% rename from apis/request/v1alpha2/request_types.go rename to apis/cluster/request/v1alpha2/request_types.go diff --git a/apis/request/v1alpha2/status_setters.go b/apis/cluster/request/v1alpha2/status_setters.go similarity index 100% rename from apis/request/v1alpha2/status_setters.go rename to apis/cluster/request/v1alpha2/status_setters.go diff --git a/apis/request/v1alpha2/zz_generated.deepcopy.go b/apis/cluster/request/v1alpha2/zz_generated.deepcopy.go similarity index 100% rename from apis/request/v1alpha2/zz_generated.deepcopy.go rename to apis/cluster/request/v1alpha2/zz_generated.deepcopy.go diff --git a/apis/request/v1alpha2/zz_generated.managed.go b/apis/cluster/request/v1alpha2/zz_generated.managed.go similarity index 96% rename from apis/request/v1alpha2/zz_generated.managed.go rename to apis/cluster/request/v1alpha2/zz_generated.managed.go index 804c4a4..aca48e0 100644 --- a/apis/request/v1alpha2/zz_generated.managed.go +++ b/apis/cluster/request/v1alpha2/zz_generated.managed.go @@ -1,71 +1,71 @@ -/* -Copyright 2020 The Crossplane Authors. - -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. -*/ - -// Code generated by angryjet. DO NOT EDIT. - -package v1alpha2 - -import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" - -// GetCondition of this Request. -func (mg *Request) GetCondition(ct xpv1.ConditionType) xpv1.Condition { - return mg.Status.GetCondition(ct) -} - -// GetDeletionPolicy of this Request. -func (mg *Request) GetDeletionPolicy() xpv1.DeletionPolicy { - return mg.Spec.DeletionPolicy -} - -// GetManagementPolicies of this Request. -func (mg *Request) GetManagementPolicies() xpv1.ManagementPolicies { - return mg.Spec.ManagementPolicies -} - -// GetProviderConfigReference of this Request. -func (mg *Request) GetProviderConfigReference() *xpv1.Reference { - return mg.Spec.ProviderConfigReference -} - -// GetWriteConnectionSecretToReference of this Request. -func (mg *Request) GetWriteConnectionSecretToReference() *xpv1.SecretReference { - return mg.Spec.WriteConnectionSecretToReference -} - -// SetConditions of this Request. -func (mg *Request) SetConditions(c ...xpv1.Condition) { - mg.Status.SetConditions(c...) -} - -// SetDeletionPolicy of this Request. -func (mg *Request) SetDeletionPolicy(r xpv1.DeletionPolicy) { - mg.Spec.DeletionPolicy = r -} - -// SetManagementPolicies of this Request. -func (mg *Request) SetManagementPolicies(r xpv1.ManagementPolicies) { - mg.Spec.ManagementPolicies = r -} - -// SetProviderConfigReference of this Request. -func (mg *Request) SetProviderConfigReference(r *xpv1.Reference) { - mg.Spec.ProviderConfigReference = r -} - -// SetWriteConnectionSecretToReference of this Request. -func (mg *Request) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { - mg.Spec.WriteConnectionSecretToReference = r -} \ No newline at end of file +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ + +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha2 + +import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + +// GetCondition of this Request. +func (mg *Request) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this Request. +func (mg *Request) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this Request. +func (mg *Request) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this Request. +func (mg *Request) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetWriteConnectionSecretToReference of this Request. +func (mg *Request) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this Request. +func (mg *Request) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this Request. +func (mg *Request) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this Request. +func (mg *Request) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this Request. +func (mg *Request) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetWriteConnectionSecretToReference of this Request. +func (mg *Request) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} diff --git a/apis/v1alpha1/doc.go b/apis/cluster/v1alpha1/doc.go similarity index 100% rename from apis/v1alpha1/doc.go rename to apis/cluster/v1alpha1/doc.go diff --git a/apis/v1alpha1/groupversion_info.go b/apis/cluster/v1alpha1/groupversion_info.go similarity index 100% rename from apis/v1alpha1/groupversion_info.go rename to apis/cluster/v1alpha1/groupversion_info.go diff --git a/apis/v1alpha1/providerconfig_types.go b/apis/cluster/v1alpha1/providerconfig_types.go similarity index 100% rename from apis/v1alpha1/providerconfig_types.go rename to apis/cluster/v1alpha1/providerconfig_types.go diff --git a/apis/v1alpha1/providerconfigusage_types.go b/apis/cluster/v1alpha1/providerconfigusage_types.go similarity index 100% rename from apis/v1alpha1/providerconfigusage_types.go rename to apis/cluster/v1alpha1/providerconfigusage_types.go diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/cluster/v1alpha1/zz_generated.deepcopy.go similarity index 100% rename from apis/v1alpha1/zz_generated.deepcopy.go rename to apis/cluster/v1alpha1/zz_generated.deepcopy.go diff --git a/apis/http.go b/apis/http.go index c535d7c..702580f 100644 --- a/apis/http.go +++ b/apis/http.go @@ -20,17 +20,25 @@ package apis import ( "k8s.io/apimachinery/pkg/runtime" - disposablerequestv1alpha2 "github.com/crossplane-contrib/provider-http/apis/disposablerequest/v1alpha2" - requestv1alpha2 "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" - httpv1alpha1 "github.com/crossplane-contrib/provider-http/apis/v1alpha1" + clusterdisposablerequestv1alpha2 "github.com/crossplane-contrib/provider-http/apis/cluster/disposablerequest/v1alpha2" + clusterrequestv1alpha2 "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + httpv1alpha1 "github.com/crossplane-contrib/provider-http/apis/cluster/v1alpha1" + namespaceddisposablerequestv1alpha2 "github.com/crossplane-contrib/provider-http/apis/namespaced/disposablerequest/v1alpha2" + namespacedrequestv1alpha2 "github.com/crossplane-contrib/provider-http/apis/namespaced/request/v1alpha2" + namespacedv1alpha2 "github.com/crossplane-contrib/provider-http/apis/namespaced/v1alpha2" ) func init() { // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back AddToSchemes = append(AddToSchemes, + // Cluster-scoped APIs httpv1alpha1.SchemeBuilder.AddToScheme, - disposablerequestv1alpha2.SchemeBuilder.AddToScheme, - requestv1alpha2.SchemeBuilder.AddToScheme, + clusterdisposablerequestv1alpha2.SchemeBuilder.AddToScheme, + clusterrequestv1alpha2.SchemeBuilder.AddToScheme, + // Namespaced APIs + namespacedv1alpha2.SchemeBuilder.AddToScheme, + namespaceddisposablerequestv1alpha2.SchemeBuilder.AddToScheme, + namespacedrequestv1alpha2.SchemeBuilder.AddToScheme, ) } diff --git a/apis/namespaced/disposablerequest/v1alpha2/disposablerequest_types.go b/apis/namespaced/disposablerequest/v1alpha2/disposablerequest_types.go new file mode 100644 index 0000000..5bd97e7 --- /dev/null +++ b/apis/namespaced/disposablerequest/v1alpha2/disposablerequest_types.go @@ -0,0 +1,133 @@ +/* +Copyright 2022 The Crossplane Authors. + +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 v1alpha2 + +import ( + "reflect" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/crossplane-contrib/provider-http/apis/common" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" +) + +// DisposableRequestParameters are the configurable fields of a DisposableRequest. +type DisposableRequestParameters struct { + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'forProvider.url' is immutable" + URL string `json:"url"` + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'forProvider.method' is immutable" + Method string `json:"method"` + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'forProvider.headers' is immutable" + Headers map[string][]string `json:"headers,omitempty"` + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'forProvider.body' is immutable" + Body string `json:"body,omitempty"` + + // WaitTimeout specifies the maximum time duration for waiting. + WaitTimeout *metav1.Duration `json:"waitTimeout,omitempty"` + + // RollbackRetriesLimit is max number of attempts to retry HTTP request by sending again the request. + RollbackRetriesLimit *int32 `json:"rollbackRetriesLimit,omitempty"` + + // InsecureSkipTLSVerify, when set to true, skips TLS certificate checks for the HTTP request + InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"` + + // ExpectedResponse is a jq filter expression used to evaluate the HTTP response and determine if it matches the expected criteria. + // The expression should return a boolean; if true, the response is considered expected. + // Example: '.body.job_status == "success"' + ExpectedResponse string `json:"expectedResponse,omitempty"` + + // NextReconcile specifies the duration after which the next reconcile should occur. + NextReconcile *metav1.Duration `json:"nextReconcile,omitempty"` + + // ShouldLoopInfinitely specifies whether the reconciliation should loop indefinitely. + ShouldLoopInfinitely bool `json:"shouldLoopInfinitely,omitempty"` + + // SecretInjectionConfig specifies the secrets receiving patches from response data. + SecretInjectionConfigs []common.SecretInjectionConfig `json:"secretInjectionConfigs,omitempty"` +} + +// A DisposableRequestSpec defines the desired state of a DisposableRequest. +type DisposableRequestSpec struct { + xpv1.ResourceSpec `json:",inline"` + ForProvider DisposableRequestParameters `json:"forProvider"` +} + +type Response struct { + StatusCode int `json:"statusCode,omitempty"` + Body string `json:"body,omitempty"` + Headers map[string][]string `json:"headers,omitempty"` +} + +type Mapping struct { + Method string `json:"method"` + Body string `json:"body,omitempty"` + URL string `json:"url"` + Headers map[string][]string `json:"headers,omitempty"` +} + +// A DisposableRequestStatus represents the observed state of a DisposableRequest. +type DisposableRequestStatus struct { + xpv1.ResourceStatus `json:",inline"` + Response Response `json:"response,omitempty"` + Failed int32 `json:"failed,omitempty"` + Error string `json:"error,omitempty"` + Synced bool `json:"synced,omitempty"` + RequestDetails Mapping `json:"requestDetails,omitempty"` + + // LastReconcileTime records the last time the resource was reconciled. + LastReconcileTime metav1.Time `json:"lastReconcileTime,omitempty"` +} + +// +kubebuilder:object:root=true + +// A DisposableRequest is a namespaced HTTP disposable request resource. +// +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" +// +kubebuilder:printcolumn:name="EXTERNAL-NAME",type="string",JSONPath=".metadata.annotations.crossplane\\.io/external-name" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,categories={crossplane,managed,http} +// +kubebuilder:storageversion +type DisposableRequest struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DisposableRequestSpec `json:"spec"` + Status DisposableRequestStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// DisposableRequestList contains a list of DisposableRequest +type DisposableRequestList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DisposableRequest `json:"items"` +} + +// DisposableRequest type metadata. +var ( + DisposableRequestKind = reflect.TypeOf(DisposableRequest{}).Name() + DisposableRequestGroupKind = schema.GroupKind{Group: Group, Kind: DisposableRequestKind}.String() + DisposableRequestKindAPIVersion = DisposableRequestKind + "." + SchemeGroupVersion.String() + DisposableRequestGroupVersionKind = SchemeGroupVersion.WithKind(DisposableRequestKind) +) + +func init() { + SchemeBuilder.Register(&DisposableRequest{}, &DisposableRequestList{}) +} diff --git a/apis/namespaced/disposablerequest/v1alpha2/doc.go b/apis/namespaced/disposablerequest/v1alpha2/doc.go new file mode 100644 index 0000000..f536071 --- /dev/null +++ b/apis/namespaced/disposablerequest/v1alpha2/doc.go @@ -0,0 +1,21 @@ +/* +Copyright 2020 The Crossplane Authors. + +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 v1alpha2 contains managed resources for http provider. +// +kubebuilder:object:generate=true +// +groupName=http.m.crossplane.io +// +versionName=v1alpha2 +package v1alpha2 diff --git a/apis/namespaced/disposablerequest/v1alpha2/groupversion_info.go b/apis/namespaced/disposablerequest/v1alpha2/groupversion_info.go new file mode 100644 index 0000000..e4506ca --- /dev/null +++ b/apis/namespaced/disposablerequest/v1alpha2/groupversion_info.go @@ -0,0 +1,40 @@ +/* +Copyright 2020 The Crossplane Authors. + +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 v1alpha2 contains the v1alpha2 group Sample resources of the http provider. +// +kubebuilder:object:generate=true +// +groupName=http.m.crossplane.io +// +versionName=v1alpha2 +package v1alpha2 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +// Package type metadata. +const ( + Group = "http.m.crossplane.io" + Version = "v1alpha2" +) + +var ( + // SchemeGroupVersion is group version used to register these objects + SchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} +) diff --git a/apis/namespaced/disposablerequest/v1alpha2/status_setters.go b/apis/namespaced/disposablerequest/v1alpha2/status_setters.go new file mode 100644 index 0000000..3bb6f46 --- /dev/null +++ b/apis/namespaced/disposablerequest/v1alpha2/status_setters.go @@ -0,0 +1,44 @@ +package v1alpha2 + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (d *DisposableRequest) SetStatusCode(statusCode int) { + d.Status.Response.StatusCode = statusCode +} + +func (d *DisposableRequest) SetHeaders(headers map[string][]string) { + d.Status.Response.Headers = headers +} + +func (d *DisposableRequest) SetBody(body string) { + d.Status.Response.Body = body +} + +func (d *DisposableRequest) SetSynced(synced bool) { + d.Status.Synced = synced + d.Status.Failed = 0 + d.Status.Error = "" +} + +func (d *DisposableRequest) SetLastReconcileTime() { + d.Status.LastReconcileTime = metav1.NewTime(time.Now()) +} + +func (d *DisposableRequest) SetError(err error) { + d.Status.Failed++ + d.Status.Synced = false + if err != nil { + d.Status.Error = err.Error() + } +} + +func (d *DisposableRequest) SetRequestDetails(url, method, body string, headers map[string][]string) { + d.Status.RequestDetails.Body = body + d.Status.RequestDetails.URL = url + d.Status.RequestDetails.Headers = headers + d.Status.RequestDetails.Method = method +} diff --git a/apis/namespaced/disposablerequest/v1alpha2/zz_generated.deepcopy.go b/apis/namespaced/disposablerequest/v1alpha2/zz_generated.deepcopy.go new file mode 100644 index 0000000..d5f1e0a --- /dev/null +++ b/apis/namespaced/disposablerequest/v1alpha2/zz_generated.deepcopy.go @@ -0,0 +1,237 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "github.com/crossplane-contrib/provider-http/apis/common" + "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DisposableRequest) DeepCopyInto(out *DisposableRequest) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DisposableRequest. +func (in *DisposableRequest) DeepCopy() *DisposableRequest { + if in == nil { + return nil + } + out := new(DisposableRequest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DisposableRequest) 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 *DisposableRequestList) DeepCopyInto(out *DisposableRequestList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DisposableRequest, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DisposableRequestList. +func (in *DisposableRequestList) DeepCopy() *DisposableRequestList { + if in == nil { + return nil + } + out := new(DisposableRequestList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DisposableRequestList) 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 *DisposableRequestParameters) DeepCopyInto(out *DisposableRequestParameters) { + *out = *in + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make(map[string][]string, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + if in.WaitTimeout != nil { + in, out := &in.WaitTimeout, &out.WaitTimeout + *out = new(v1.Duration) + **out = **in + } + if in.RollbackRetriesLimit != nil { + in, out := &in.RollbackRetriesLimit, &out.RollbackRetriesLimit + *out = new(int32) + **out = **in + } + if in.NextReconcile != nil { + in, out := &in.NextReconcile, &out.NextReconcile + *out = new(v1.Duration) + **out = **in + } + if in.SecretInjectionConfigs != nil { + in, out := &in.SecretInjectionConfigs, &out.SecretInjectionConfigs + *out = make([]common.SecretInjectionConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DisposableRequestParameters. +func (in *DisposableRequestParameters) DeepCopy() *DisposableRequestParameters { + if in == nil { + return nil + } + out := new(DisposableRequestParameters) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DisposableRequestSpec) DeepCopyInto(out *DisposableRequestSpec) { + *out = *in + in.ResourceSpec.DeepCopyInto(&out.ResourceSpec) + in.ForProvider.DeepCopyInto(&out.ForProvider) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DisposableRequestSpec. +func (in *DisposableRequestSpec) DeepCopy() *DisposableRequestSpec { + if in == nil { + return nil + } + out := new(DisposableRequestSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DisposableRequestStatus) DeepCopyInto(out *DisposableRequestStatus) { + *out = *in + in.ResourceStatus.DeepCopyInto(&out.ResourceStatus) + in.Response.DeepCopyInto(&out.Response) + in.RequestDetails.DeepCopyInto(&out.RequestDetails) + in.LastReconcileTime.DeepCopyInto(&out.LastReconcileTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DisposableRequestStatus. +func (in *DisposableRequestStatus) DeepCopy() *DisposableRequestStatus { + if in == nil { + return nil + } + out := new(DisposableRequestStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Mapping) DeepCopyInto(out *Mapping) { + *out = *in + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make(map[string][]string, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Mapping. +func (in *Mapping) DeepCopy() *Mapping { + if in == nil { + return nil + } + out := new(Mapping) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Response) DeepCopyInto(out *Response) { + *out = *in + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make(map[string][]string, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Response. +func (in *Response) DeepCopy() *Response { + if in == nil { + return nil + } + out := new(Response) + in.DeepCopyInto(out) + return out +} diff --git a/apis/namespaced/disposablerequest/v1alpha2/zz_generated.managed.go b/apis/namespaced/disposablerequest/v1alpha2/zz_generated.managed.go new file mode 100644 index 0000000..08ed963 --- /dev/null +++ b/apis/namespaced/disposablerequest/v1alpha2/zz_generated.managed.go @@ -0,0 +1,71 @@ +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ + +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha2 + +import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + +// GetCondition of this DisposableRequest. +func (mg *DisposableRequest) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this DisposableRequest. +func (mg *DisposableRequest) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this DisposableRequest. +func (mg *DisposableRequest) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this DisposableRequest. +func (mg *DisposableRequest) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetWriteConnectionSecretToReference of this DisposableRequest. +func (mg *DisposableRequest) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this DisposableRequest. +func (mg *DisposableRequest) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this DisposableRequest. +func (mg *DisposableRequest) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this DisposableRequest. +func (mg *DisposableRequest) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this DisposableRequest. +func (mg *DisposableRequest) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetWriteConnectionSecretToReference of this DisposableRequest. +func (mg *DisposableRequest) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} diff --git a/apis/namespaced/request/v1alpha2/doc.go b/apis/namespaced/request/v1alpha2/doc.go new file mode 100644 index 0000000..f536071 --- /dev/null +++ b/apis/namespaced/request/v1alpha2/doc.go @@ -0,0 +1,21 @@ +/* +Copyright 2020 The Crossplane Authors. + +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 v1alpha2 contains managed resources for http provider. +// +kubebuilder:object:generate=true +// +groupName=http.m.crossplane.io +// +versionName=v1alpha2 +package v1alpha2 diff --git a/apis/namespaced/request/v1alpha2/groupversion_info.go b/apis/namespaced/request/v1alpha2/groupversion_info.go new file mode 100644 index 0000000..e4506ca --- /dev/null +++ b/apis/namespaced/request/v1alpha2/groupversion_info.go @@ -0,0 +1,40 @@ +/* +Copyright 2020 The Crossplane Authors. + +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 v1alpha2 contains the v1alpha2 group Sample resources of the http provider. +// +kubebuilder:object:generate=true +// +groupName=http.m.crossplane.io +// +versionName=v1alpha2 +package v1alpha2 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +// Package type metadata. +const ( + Group = "http.m.crossplane.io" + Version = "v1alpha2" +) + +var ( + // SchemeGroupVersion is group version used to register these objects + SchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} +) diff --git a/apis/namespaced/request/v1alpha2/request_types.go b/apis/namespaced/request/v1alpha2/request_types.go new file mode 100644 index 0000000..2833320 --- /dev/null +++ b/apis/namespaced/request/v1alpha2/request_types.go @@ -0,0 +1,171 @@ +/* +Copyright 2022 The Crossplane Authors. + +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 v1alpha2 + +import ( + "reflect" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/crossplane-contrib/provider-http/apis/common" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" +) + +const ( + ExpectedResponseCheckTypeDefault = "DEFAULT" + ExpectedResponseCheckTypeCustom = "CUSTOM" +) + +const ( + ActionCreate = "CREATE" + ActionObserve = "OBSERVE" + ActionUpdate = "UPDATE" + ActionRemove = "REMOVE" +) + +// RequestParameters are the configurable fields of a Request. +type RequestParameters struct { + // Mappings defines the HTTP mappings for different methods. + // Either Method or Action must be specified. If both are omitted, the mapping will not be used. + // +kubebuilder:validation:MinItems=1 + Mappings []Mapping `json:"mappings"` + + // Payload defines the payload for the request. + Payload Payload `json:"payload"` + + // Headers defines default headers for each request. + Headers map[string][]string `json:"headers,omitempty"` + + // WaitTimeout specifies the maximum time duration for waiting. + WaitTimeout *metav1.Duration `json:"waitTimeout,omitempty"` + + // InsecureSkipTLSVerify, when set to true, skips TLS certificate checks for the HTTP request + InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"` + + // SecretInjectionConfig specifies the secrets receiving patches for response data. + SecretInjectionConfigs []common.SecretInjectionConfig `json:"secretInjectionConfigs,omitempty"` + + // ExpectedResponseCheck specifies the mechanism to validate the OBSERVE response against expected value. + ExpectedResponseCheck ExpectedResponseCheck `json:"expectedResponseCheck,omitempty"` + + // IsRemovedCheck specifies the mechanism to validate the OBSERVE response after removal against expected value. + IsRemovedCheck ExpectedResponseCheck `json:"isRemovedCheck,omitempty"` +} + +type Mapping struct { + // +kubebuilder:validation:Enum=POST;GET;PUT;DELETE;PATCH;HEAD;OPTIONS + // Method specifies the HTTP method for the request. + Method string `json:"method,omitempty"` + + // +kubebuilder:validation:Enum=CREATE;OBSERVE;UPDATE;REMOVE + // Action specifies the intended action for the request. + Action string `json:"action,omitempty"` + + // Body specifies the body of the request. + Body string `json:"body,omitempty"` + + // URL specifies the URL for the request. + URL string `json:"url"` + + // Headers specifies the headers for the request. + Headers map[string][]string `json:"headers,omitempty"` +} + +type ExpectedResponseCheck struct { + // Type specifies the type of the expected response check. + // +kubebuilder:validation:Enum=DEFAULT;CUSTOM + Type string `json:"type,omitempty"` + + // Logic specifies the custom logic for the expected response check. + Logic string `json:"logic,omitempty"` +} + +type Payload struct { + // BaseUrl specifies the base URL for the request. + BaseUrl string `json:"baseUrl,omitempty"` + + // Body specifies data to be used in the request body. + Body string `json:"body,omitempty"` +} + +// A RequestSpec defines the desired state of a Request. +type RequestSpec struct { + xpv1.ResourceSpec `json:",inline"` + ForProvider RequestParameters `json:"forProvider"` +} + +// RequestObservation are the observable fields of a Request. +type Response struct { + StatusCode int `json:"statusCode,omitempty"` + Body string `json:"body,omitempty"` + Headers map[string][]string `json:"headers,omitempty"` +} + +// A RequestStatus represents the observed state of a Request. +type RequestStatus struct { + xpv1.ResourceStatus `json:",inline"` + Response Response `json:"response,omitempty"` + Cache Cache `json:"cache,omitempty"` + Failed int32 `json:"failed,omitempty"` + Error string `json:"error,omitempty"` + RequestDetails Mapping `json:"requestDetails,omitempty"` +} + +type Cache struct { + LastUpdated string `json:"lastUpdated,omitempty"` + Response Response `json:"response,omitempty"` +} + +// +kubebuilder:object:root=true + +// A Request is a namespaced HTTP request resource. +// +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" +// +kubebuilder:printcolumn:name="EXTERNAL-NAME",type="string",JSONPath=".metadata.annotations.crossplane\\.io/external-name" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,categories={crossplane,managed,http} +// +kubebuilder:storageversion +type Request struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RequestSpec `json:"spec"` + Status RequestStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// RequestList contains a list of Request +type RequestList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Request `json:"items"` +} + +// Request type metadata. +var ( + RequestKind = reflect.TypeOf(Request{}).Name() + RequestGroupKind = schema.GroupKind{Group: Group, Kind: RequestKind}.String() + RequestKindAPIVersion = RequestKind + "." + SchemeGroupVersion.String() + RequestGroupVersionKind = SchemeGroupVersion.WithKind(RequestKind) +) + +func init() { + SchemeBuilder.Register(&Request{}, &RequestList{}) +} diff --git a/apis/namespaced/request/v1alpha2/status_setters.go b/apis/namespaced/request/v1alpha2/status_setters.go new file mode 100644 index 0000000..bb4ae7f --- /dev/null +++ b/apis/namespaced/request/v1alpha2/status_setters.go @@ -0,0 +1,41 @@ +package v1alpha2 + +import "time" + +func (d *Request) SetStatusCode(statusCode int) { + d.Status.Response.StatusCode = statusCode +} + +func (d *Request) SetHeaders(headers map[string][]string) { + d.Status.Response.Headers = headers +} + +func (d *Request) SetBody(body string) { + d.Status.Response.Body = body +} + +func (d *Request) SetError(err error) { + d.Status.Failed++ + if err != nil { + d.Status.Error = err.Error() + } +} + +func (d *Request) ResetFailures() { + d.Status.Failed = 0 + d.Status.Error = "" +} + +func (d *Request) SetRequestDetails(url, method, body string, headers map[string][]string) { + d.Status.RequestDetails.Body = body + d.Status.RequestDetails.URL = url + d.Status.RequestDetails.Headers = headers + d.Status.RequestDetails.Method = method +} + +func (d *Request) SetCache(statusCode int, headers map[string][]string, body string) { + d.Status.Cache.Response.StatusCode = statusCode + d.Status.Cache.Response.Headers = headers + d.Status.Cache.Response.Body = body + d.Status.Cache.LastUpdated = time.Now().UTC().Format(time.RFC3339) +} diff --git a/apis/namespaced/request/v1alpha2/zz_generated.deepcopy.go b/apis/namespaced/request/v1alpha2/zz_generated.deepcopy.go new file mode 100644 index 0000000..54c2e9a --- /dev/null +++ b/apis/namespaced/request/v1alpha2/zz_generated.deepcopy.go @@ -0,0 +1,283 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "github.com/crossplane-contrib/provider-http/apis/common" + "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Cache) DeepCopyInto(out *Cache) { + *out = *in + in.Response.DeepCopyInto(&out.Response) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Cache. +func (in *Cache) DeepCopy() *Cache { + if in == nil { + return nil + } + out := new(Cache) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExpectedResponseCheck) DeepCopyInto(out *ExpectedResponseCheck) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpectedResponseCheck. +func (in *ExpectedResponseCheck) DeepCopy() *ExpectedResponseCheck { + if in == nil { + return nil + } + out := new(ExpectedResponseCheck) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Mapping) DeepCopyInto(out *Mapping) { + *out = *in + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make(map[string][]string, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Mapping. +func (in *Mapping) DeepCopy() *Mapping { + if in == nil { + return nil + } + out := new(Mapping) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Payload) DeepCopyInto(out *Payload) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Payload. +func (in *Payload) DeepCopy() *Payload { + if in == nil { + return nil + } + out := new(Payload) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Request) DeepCopyInto(out *Request) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Request. +func (in *Request) DeepCopy() *Request { + if in == nil { + return nil + } + out := new(Request) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Request) 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 *RequestList) DeepCopyInto(out *RequestList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Request, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RequestList. +func (in *RequestList) DeepCopy() *RequestList { + if in == nil { + return nil + } + out := new(RequestList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RequestList) 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 *RequestParameters) DeepCopyInto(out *RequestParameters) { + *out = *in + if in.Mappings != nil { + in, out := &in.Mappings, &out.Mappings + *out = make([]Mapping, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.Payload = in.Payload + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make(map[string][]string, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + if in.WaitTimeout != nil { + in, out := &in.WaitTimeout, &out.WaitTimeout + *out = new(v1.Duration) + **out = **in + } + if in.SecretInjectionConfigs != nil { + in, out := &in.SecretInjectionConfigs, &out.SecretInjectionConfigs + *out = make([]common.SecretInjectionConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.ExpectedResponseCheck = in.ExpectedResponseCheck + out.IsRemovedCheck = in.IsRemovedCheck +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RequestParameters. +func (in *RequestParameters) DeepCopy() *RequestParameters { + if in == nil { + return nil + } + out := new(RequestParameters) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RequestSpec) DeepCopyInto(out *RequestSpec) { + *out = *in + in.ResourceSpec.DeepCopyInto(&out.ResourceSpec) + in.ForProvider.DeepCopyInto(&out.ForProvider) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RequestSpec. +func (in *RequestSpec) DeepCopy() *RequestSpec { + if in == nil { + return nil + } + out := new(RequestSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RequestStatus) DeepCopyInto(out *RequestStatus) { + *out = *in + in.ResourceStatus.DeepCopyInto(&out.ResourceStatus) + in.Response.DeepCopyInto(&out.Response) + in.Cache.DeepCopyInto(&out.Cache) + in.RequestDetails.DeepCopyInto(&out.RequestDetails) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RequestStatus. +func (in *RequestStatus) DeepCopy() *RequestStatus { + if in == nil { + return nil + } + out := new(RequestStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Response) DeepCopyInto(out *Response) { + *out = *in + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make(map[string][]string, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Response. +func (in *Response) DeepCopy() *Response { + if in == nil { + return nil + } + out := new(Response) + in.DeepCopyInto(out) + return out +} diff --git a/apis/namespaced/request/v1alpha2/zz_generated.managed.go b/apis/namespaced/request/v1alpha2/zz_generated.managed.go new file mode 100644 index 0000000..aca48e0 --- /dev/null +++ b/apis/namespaced/request/v1alpha2/zz_generated.managed.go @@ -0,0 +1,71 @@ +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ + +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha2 + +import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + +// GetCondition of this Request. +func (mg *Request) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this Request. +func (mg *Request) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this Request. +func (mg *Request) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this Request. +func (mg *Request) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetWriteConnectionSecretToReference of this Request. +func (mg *Request) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this Request. +func (mg *Request) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this Request. +func (mg *Request) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this Request. +func (mg *Request) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this Request. +func (mg *Request) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetWriteConnectionSecretToReference of this Request. +func (mg *Request) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} diff --git a/apis/namespaced/v1alpha2/clusterproviderconfig_types.go b/apis/namespaced/v1alpha2/clusterproviderconfig_types.go new file mode 100644 index 0000000..ae51b9b --- /dev/null +++ b/apis/namespaced/v1alpha2/clusterproviderconfig_types.go @@ -0,0 +1,149 @@ +/* +Copyright 2020 The Crossplane Authors. + +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 v1alpha2 + +import ( + "reflect" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" +) + +// verify casting done in controller +var _ resource.ProviderConfig = &ClusterProviderConfig{} +var _ resource.ProviderConfigUsage = &ClusterProviderConfigUsage{} +var _ resource.ProviderConfigUsageList = &ClusterProviderConfigUsageList{} + +// +kubebuilder:object:root=true + +// A ClusterProviderConfig configures a Http provider for cross-namespace access. +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="SECRET-NAME",type="string",JSONPath=".spec.credentials.secretRef.name",priority=1 +// +kubebuilder:resource:scope=Cluster +type ClusterProviderConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ProviderConfigSpec `json:"spec"` + Status ProviderConfigStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ClusterProviderConfigList contains a list of ClusterProviderConfig. +type ClusterProviderConfigList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClusterProviderConfig `json:"items"` +} + +// +kubebuilder:object:root=true + +// A ClusterProviderConfigUsage indicates that a resource is using a ClusterProviderConfig. +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="CONFIG-NAME",type="string",JSONPath=".providerConfigRef.name" +// +kubebuilder:printcolumn:name="RESOURCE-KIND",type="string",JSONPath=".resourceRef.kind" +// +kubebuilder:printcolumn:name="RESOURCE-NAME",type="string",JSONPath=".resourceRef.name" +// +kubebuilder:resource:scope=Cluster,categories={crossplane,provider,http} +type ClusterProviderConfigUsage struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + xpv1.ProviderConfigUsage `json:",inline"` +} + +// +kubebuilder:object:root=true + +// ClusterProviderConfigUsageList contains a list of ClusterProviderConfigUsage +type ClusterProviderConfigUsageList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClusterProviderConfigUsage `json:"items"` +} + +// ClusterProviderConfig type metadata. +var ( + ClusterProviderConfigKind = reflect.TypeOf(ClusterProviderConfig{}).Name() + ClusterProviderConfigGroupKind = schema.GroupKind{Group: Group, Kind: ClusterProviderConfigKind}.String() + ClusterProviderConfigKindAPIVersion = ClusterProviderConfigKind + "." + SchemeGroupVersion.String() + ClusterProviderConfigGroupVersionKind = SchemeGroupVersion.WithKind(ClusterProviderConfigKind) +) + +// ClusterProviderConfigUsage type metadata. +var ( + ClusterProviderConfigUsageKind = reflect.TypeOf(ClusterProviderConfigUsage{}).Name() + ClusterProviderConfigUsageGroupKind = schema.GroupKind{Group: Group, Kind: ClusterProviderConfigUsageKind}.String() + ClusterProviderConfigUsageKindAPIVersion = ClusterProviderConfigUsageKind + "." + SchemeGroupVersion.String() + ClusterProviderConfigUsageGroupVersionKind = SchemeGroupVersion.WithKind(ClusterProviderConfigUsageKind) + ClusterProviderConfigUsageListKind = reflect.TypeOf(ClusterProviderConfigUsageList{}).Name() + ClusterProviderConfigUsageListGroupVersionKind = SchemeGroupVersion.WithKind(ClusterProviderConfigUsageListKind) +) + +// ClusterProviderConfig interface methods + +// GetCondition returns the condition for the given ConditionType if exists, +// otherwise returns nil +func (cpc *ClusterProviderConfig) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return cpc.Status.GetCondition(ct) +} + +// SetConditions sets the conditions on the resource status +func (cpc *ClusterProviderConfig) SetConditions(c ...xpv1.Condition) { + cpc.Status.SetConditions(c...) +} + +// GetUsers returns the number of users of this ClusterProviderConfig. +func (cpc *ClusterProviderConfig) GetUsers() int64 { + return cpc.Status.Users +} + +// SetUsers sets the number of users of this ClusterProviderConfig. +func (cpc *ClusterProviderConfig) SetUsers(i int64) { + cpc.Status.Users = i +} + +// ClusterProviderConfigUsage interface methods + +// SetResourceReference sets the resource reference. +func (cpcu *ClusterProviderConfigUsage) SetResourceReference(r xpv1.TypedReference) { + cpcu.ResourceReference = r +} + +// GetResourceReference gets the resource reference. +func (cpcu *ClusterProviderConfigUsage) GetResourceReference() xpv1.TypedReference { + return cpcu.ResourceReference +} + +// ClusterProviderConfigUsageList interface methods + +// GetItems returns the list of ClusterProviderConfigUsage items. +func (cpcul *ClusterProviderConfigUsageList) GetItems() []resource.ProviderConfigUsage { + items := make([]resource.ProviderConfigUsage, len(cpcul.Items)) + for i := range cpcul.Items { + items[i] = &cpcul.Items[i] + } + return items +} + +func init() { + SchemeBuilder.Register(&ClusterProviderConfig{}, &ClusterProviderConfigList{}) + SchemeBuilder.Register(&ClusterProviderConfigUsage{}, &ClusterProviderConfigUsageList{}) +} diff --git a/apis/namespaced/v1alpha2/doc.go b/apis/namespaced/v1alpha2/doc.go new file mode 100644 index 0000000..1d409f7 --- /dev/null +++ b/apis/namespaced/v1alpha2/doc.go @@ -0,0 +1,21 @@ +/* +Copyright 2020 The Crossplane Authors. + +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 v1alpha2 contains namespaced provider configurations for http provider. +// +kubebuilder:object:generate=true +// +groupName=http.m.crossplane.io +// +versionName=v1alpha2 +package v1alpha2 diff --git a/apis/namespaced/v1alpha2/groupversion_info.go b/apis/namespaced/v1alpha2/groupversion_info.go new file mode 100644 index 0000000..01e1837 --- /dev/null +++ b/apis/namespaced/v1alpha2/groupversion_info.go @@ -0,0 +1,40 @@ +/* +Copyright 2020 The Crossplane Authors. + +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 v1alpha2 contains the v1alpha2 group namespaced provider configurations of the http provider. +// +kubebuilder:object:generate=true +// +groupName=http.m.crossplane.io +// +versionName=v1alpha2 +package v1alpha2 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +// Package type metadata. +const ( + Group = "http.m.crossplane.io" + Version = "v1alpha2" +) + +var ( + // SchemeGroupVersion is group version used to register these objects + SchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} +) diff --git a/apis/namespaced/v1alpha2/providerconfig_types.go b/apis/namespaced/v1alpha2/providerconfig_types.go new file mode 100644 index 0000000..c849740 --- /dev/null +++ b/apis/namespaced/v1alpha2/providerconfig_types.go @@ -0,0 +1,169 @@ +/* +Copyright 2020 The Crossplane Authors. + +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 v1alpha2 + +import ( + "reflect" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" +) + +// verify casting done in controller +var _ resource.ProviderConfig = &ProviderConfig{} +var _ resource.ProviderConfigUsage = &ProviderConfigUsage{} +var _ resource.ProviderConfigUsageList = &ProviderConfigUsageList{} + +// A ProviderConfigSpec defines the desired state of a ProviderConfig. +type ProviderConfigSpec struct { + // Credentials required to authenticate to this provider. + Credentials ProviderCredentials `json:"credentials"` +} + +// ProviderCredentials required to authenticate. +type ProviderCredentials struct { + // Source of the provider credentials. + // +kubebuilder:validation:Enum=None;Secret;InjectedIdentity;Environment;Filesystem + Source xpv1.CredentialsSource `json:"source"` + + xpv1.CommonCredentialSelectors `json:",inline"` +} + +// A ProviderConfigStatus reflects the observed state of a ProviderConfig. +type ProviderConfigStatus struct { + xpv1.ProviderConfigStatus `json:",inline"` +} + +// +kubebuilder:object:root=true + +// A ProviderConfig configures a Http provider for namespaced resources. +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="SECRET-NAME",type="string",JSONPath=".spec.credentials.secretRef.name",priority=1 +// +kubebuilder:resource:scope=Namespaced +type ProviderConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ProviderConfigSpec `json:"spec"` + Status ProviderConfigStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ProviderConfigList contains a list of ProviderConfig. +type ProviderConfigList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ProviderConfig `json:"items"` +} + +// +kubebuilder:object:root=true + +// A ProviderConfigUsage indicates that a resource is using a ProviderConfig. +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="CONFIG-NAME",type="string",JSONPath=".providerConfigRef.name" +// +kubebuilder:printcolumn:name="RESOURCE-KIND",type="string",JSONPath=".resourceRef.kind" +// +kubebuilder:printcolumn:name="RESOURCE-NAME",type="string",JSONPath=".resourceRef.name" +// +kubebuilder:resource:scope=Namespaced,categories={crossplane,provider,http} +type ProviderConfigUsage struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + xpv1.ProviderConfigUsage `json:",inline"` +} + +// +kubebuilder:object:root=true + +// ProviderConfigUsageList contains a list of ProviderConfigUsage +type ProviderConfigUsageList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ProviderConfigUsage `json:"items"` +} + +// ProviderConfig type metadata. +var ( + ProviderConfigKind = reflect.TypeOf(ProviderConfig{}).Name() + ProviderConfigGroupKind = schema.GroupKind{Group: Group, Kind: ProviderConfigKind}.String() + ProviderConfigKindAPIVersion = ProviderConfigKind + "." + SchemeGroupVersion.String() + ProviderConfigGroupVersionKind = SchemeGroupVersion.WithKind(ProviderConfigKind) +) + +// ProviderConfigUsage type metadata. +var ( + ProviderConfigUsageKind = reflect.TypeOf(ProviderConfigUsage{}).Name() + ProviderConfigUsageGroupKind = schema.GroupKind{Group: Group, Kind: ProviderConfigUsageKind}.String() + ProviderConfigUsageKindAPIVersion = ProviderConfigUsageKind + "." + SchemeGroupVersion.String() + ProviderConfigUsageGroupVersionKind = SchemeGroupVersion.WithKind(ProviderConfigUsageKind) + ProviderConfigUsageListKind = reflect.TypeOf(ProviderConfigUsageList{}).Name() + ProviderConfigUsageListGroupVersionKind = SchemeGroupVersion.WithKind(ProviderConfigUsageListKind) +) + +// ProviderConfig interface methods + +// GetCondition returns the condition for the given ConditionType if exists, +// otherwise returns nil +func (pc *ProviderConfig) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return pc.Status.GetCondition(ct) +} + +// SetConditions sets the conditions on the resource status +func (pc *ProviderConfig) SetConditions(c ...xpv1.Condition) { + pc.Status.SetConditions(c...) +} + +// GetUsers returns the number of users of this ProviderConfig. +func (pc *ProviderConfig) GetUsers() int64 { + return pc.Status.Users +} + +// SetUsers sets the number of users of this ProviderConfig. +func (pc *ProviderConfig) SetUsers(i int64) { + pc.Status.Users = i +} + +// ProviderConfigUsage interface methods + +// SetResourceReference sets the resource reference. +func (pcu *ProviderConfigUsage) SetResourceReference(r xpv1.TypedReference) { + pcu.ResourceReference = r +} + +// GetResourceReference gets the resource reference. +func (pcu *ProviderConfigUsage) GetResourceReference() xpv1.TypedReference { + return pcu.ResourceReference +} + +// ProviderConfigUsageList interface methods + +// GetItems returns the list of ProviderConfigUsage items. +func (pcul *ProviderConfigUsageList) GetItems() []resource.ProviderConfigUsage { + items := make([]resource.ProviderConfigUsage, len(pcul.Items)) + for i := range pcul.Items { + items[i] = &pcul.Items[i] + } + return items +} + +func init() { + SchemeBuilder.Register(&ProviderConfig{}, &ProviderConfigList{}) + SchemeBuilder.Register(&ProviderConfigUsage{}, &ProviderConfigUsageList{}) +} diff --git a/apis/namespaced/v1alpha2/zz_generated.deepcopy.go b/apis/namespaced/v1alpha2/zz_generated.deepcopy.go new file mode 100644 index 0000000..a067d52 --- /dev/null +++ b/apis/namespaced/v1alpha2/zz_generated.deepcopy.go @@ -0,0 +1,307 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterProviderConfig) DeepCopyInto(out *ClusterProviderConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterProviderConfig. +func (in *ClusterProviderConfig) DeepCopy() *ClusterProviderConfig { + if in == nil { + return nil + } + out := new(ClusterProviderConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterProviderConfig) 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 *ClusterProviderConfigList) DeepCopyInto(out *ClusterProviderConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterProviderConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterProviderConfigList. +func (in *ClusterProviderConfigList) DeepCopy() *ClusterProviderConfigList { + if in == nil { + return nil + } + out := new(ClusterProviderConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterProviderConfigList) 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 *ClusterProviderConfigUsage) DeepCopyInto(out *ClusterProviderConfigUsage) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.ProviderConfigUsage.DeepCopyInto(&out.ProviderConfigUsage) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterProviderConfigUsage. +func (in *ClusterProviderConfigUsage) DeepCopy() *ClusterProviderConfigUsage { + if in == nil { + return nil + } + out := new(ClusterProviderConfigUsage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterProviderConfigUsage) 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 *ClusterProviderConfigUsageList) DeepCopyInto(out *ClusterProviderConfigUsageList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterProviderConfigUsage, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterProviderConfigUsageList. +func (in *ClusterProviderConfigUsageList) DeepCopy() *ClusterProviderConfigUsageList { + if in == nil { + return nil + } + out := new(ClusterProviderConfigUsageList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterProviderConfigUsageList) 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 *ProviderConfig) DeepCopyInto(out *ProviderConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfig. +func (in *ProviderConfig) DeepCopy() *ProviderConfig { + if in == nil { + return nil + } + out := new(ProviderConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ProviderConfig) 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 *ProviderConfigList) DeepCopyInto(out *ProviderConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ProviderConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigList. +func (in *ProviderConfigList) DeepCopy() *ProviderConfigList { + if in == nil { + return nil + } + out := new(ProviderConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ProviderConfigList) 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 *ProviderConfigSpec) DeepCopyInto(out *ProviderConfigSpec) { + *out = *in + in.Credentials.DeepCopyInto(&out.Credentials) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigSpec. +func (in *ProviderConfigSpec) DeepCopy() *ProviderConfigSpec { + if in == nil { + return nil + } + out := new(ProviderConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderConfigStatus) DeepCopyInto(out *ProviderConfigStatus) { + *out = *in + in.ProviderConfigStatus.DeepCopyInto(&out.ProviderConfigStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigStatus. +func (in *ProviderConfigStatus) DeepCopy() *ProviderConfigStatus { + if in == nil { + return nil + } + out := new(ProviderConfigStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderConfigUsage) DeepCopyInto(out *ProviderConfigUsage) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.ProviderConfigUsage.DeepCopyInto(&out.ProviderConfigUsage) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigUsage. +func (in *ProviderConfigUsage) DeepCopy() *ProviderConfigUsage { + if in == nil { + return nil + } + out := new(ProviderConfigUsage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ProviderConfigUsage) 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 *ProviderConfigUsageList) DeepCopyInto(out *ProviderConfigUsageList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ProviderConfigUsage, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigUsageList. +func (in *ProviderConfigUsageList) DeepCopy() *ProviderConfigUsageList { + if in == nil { + return nil + } + out := new(ProviderConfigUsageList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ProviderConfigUsageList) 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 *ProviderCredentials) DeepCopyInto(out *ProviderCredentials) { + *out = *in + in.CommonCredentialSelectors.DeepCopyInto(&out.CommonCredentialSelectors) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderCredentials. +func (in *ProviderCredentials) DeepCopy() *ProviderCredentials { + if in == nil { + return nil + } + out := new(ProviderCredentials) + in.DeepCopyInto(out) + return out +} diff --git a/cmd/provider/main.go b/cmd/provider/main.go index 2cedde1..b837c3f 100644 --- a/cmd/provider/main.go +++ b/cmd/provider/main.go @@ -21,6 +21,8 @@ import ( "path/filepath" "time" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/cache" kingpin "gopkg.in/alecthomas/kingpin.v2" @@ -30,8 +32,10 @@ import ( "github.com/crossplane/crossplane-runtime/v2/pkg/controller" "github.com/crossplane/crossplane-runtime/v2/pkg/feature" + "github.com/crossplane/crossplane-runtime/v2/pkg/gate" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/crossplane/crossplane-runtime/v2/pkg/ratelimiter" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/customresourcesgate" "github.com/crossplane-contrib/provider-http/apis" template "github.com/crossplane-contrib/provider-http/internal/controller" @@ -78,14 +82,21 @@ func main() { kingpin.FatalIfError(err, "Cannot create controller manager") kingpin.FatalIfError(apis.AddToScheme(mgr.GetScheme()), "Cannot add Http APIs to scheme") + // Add CRD types to scheme for SafeStart capability + kingpin.FatalIfError(apiextensionsv1.AddToScheme(mgr.GetScheme()), "Cannot add CRD APIs to scheme") + o := controller.Options{ Logger: log, MaxConcurrentReconciles: *maxReconcileRate, PollInterval: *pollInterval, GlobalRateLimiter: ratelimiter.NewGlobal(*maxReconcileRate), Features: &feature.Flags{}, + Gate: new(gate.Gate[schema.GroupVersionKind]), } - kingpin.FatalIfError(template.Setup(mgr, o, *timeout), "Cannot setup Template controllers") + // Setup SafeStart CRD gate controller + kingpin.FatalIfError(customresourcesgate.Setup(mgr, o), "Cannot setup CRD gate controller") + + kingpin.FatalIfError(template.SetupGated(mgr, o, *timeout), "Cannot setup Template controllers") kingpin.FatalIfError(mgr.Start(ctrl.SetupSignalHandler()), "Cannot start controller manager") } diff --git a/examples/namespaced/README.md b/examples/namespaced/README.md new file mode 100644 index 0000000..9d03a38 --- /dev/null +++ b/examples/namespaced/README.md @@ -0,0 +1,91 @@ +# Namespaced HTTP Provider Examples + +This directory contains examples for the namespaced version of the HTTP provider resources. These resources use the `http.m.crossplane.io` API group and provide namespace-scoped alternatives to the cluster-scoped resources in the `http.crossplane.io` API group. + +## Key Differences from Cluster-scoped Resources + +### API Group +- **Cluster-scoped**: `http.crossplane.io` +- **Namespaced**: `http.m.crossplane.io` (follows the `.m.` convention) + +### Scope +- **Cluster-scoped**: Resources are available cluster-wide +- **Namespaced**: Resources are confined to a specific namespace + +### Provider Configuration Options + +#### ProviderConfig (Namespace-scoped) +```yaml +apiVersion: http.m.crossplane.io/v1alpha2 +kind: ProviderConfig +metadata: + name: http-conf-namespaced + namespace: default # Confined to this namespace +``` + +#### ClusterProviderConfig (Cluster-scoped) +```yaml +apiVersion: http.m.crossplane.io/v1alpha2 +kind: ClusterProviderConfig +metadata: + name: http-conf-cluster # No namespace - cluster-wide +``` + +## When to Use Each Approach + +### Use Cluster-scoped Resources When: +- You need shared configuration across multiple namespaces +- You have cluster-admin privileges +- You want centralized management of HTTP resources +- Resources are shared infrastructure components + +### Use Namespaced Resources When: +- You want namespace isolation for security +- Multiple teams/tenants share the same cluster +- You have namespace-level permissions only +- Resources are application-specific + +## Examples Included + +### Provider Configurations +1. **providerconfig.yaml** - Namespace-scoped provider configuration +2. **clusterproviderconfig.yaml** - Cluster-scoped provider configuration for cross-namespace access + +### Request Examples +3. **request.yaml** - Namespaced HTTP request with full CRUD operations using namespaced ProviderConfig +4. **request-with-clusterproviderconfig.yaml** - Namespaced HTTP request using ClusterProviderConfig for cross-namespace access + +### DisposableRequest Examples +5. **disposablerequest.yaml** - Namespaced one-time HTTP request using namespaced ProviderConfig +6. **disposablerequest-jwt.yaml** - Namespaced JWT token acquisition example +7. **disposablerequest-with-clusterproviderconfig.yaml** - Namespaced one-time HTTP request using ClusterProviderConfig + +## Usage + +1. Apply the provider configuration: + ```bash + # For namespace-scoped resources + kubectl apply -f providerconfig.yaml + + # For cluster-scoped cross-namespace access + kubectl apply -f clusterproviderconfig.yaml + ``` + +2. Apply the resource examples: + ```bash + kubectl apply -f request.yaml + kubectl apply -f disposablerequest.yaml + ``` + +## Migration from Cluster-scoped Resources + +If you're migrating from cluster-scoped resources (`http.crossplane.io`), you'll need to: + +1. Update the `apiVersion` from `http.crossplane.io/v1alpha2` to `http.m.crossplane.io/v1alpha2` +2. Add a `namespace` field to the metadata +3. Update the `providerConfigRef` to reference a namespaced ProviderConfig +4. Ensure secrets referenced in `secretInjectionConfigs` are in the same namespace + +## Cross-namespace Access + +If you need to access secrets or resources in different namespaces, use a `ClusterProviderConfig` instead of a namespace-scoped `ProviderConfig`. The ClusterProviderConfig allows cross-namespace operations while the resource itself remains namespaced. \ No newline at end of file diff --git a/examples/namespaced/clusterproviderconfig.yaml b/examples/namespaced/clusterproviderconfig.yaml new file mode 100644 index 0000000..0f1a201 --- /dev/null +++ b/examples/namespaced/clusterproviderconfig.yaml @@ -0,0 +1,7 @@ +apiVersion: http.m.crossplane.io/v1alpha2 +kind: ClusterProviderConfig +metadata: + name: http-conf-cluster +spec: + credentials: + source: None \ No newline at end of file diff --git a/examples/namespaced/disposablerequest-jwt.yaml b/examples/namespaced/disposablerequest-jwt.yaml new file mode 100644 index 0000000..253680e --- /dev/null +++ b/examples/namespaced/disposablerequest-jwt.yaml @@ -0,0 +1,36 @@ +apiVersion: http.m.crossplane.io/v1alpha2 +kind: DisposableRequest +metadata: + name: obtain-jwt-token-namespaced + namespace: default +spec: + deletionPolicy: Orphan + forProvider: + insecureSkipTLSVerify: true + + # Injecting data from secrets is possible, simply use the following syntax: {{ name:namespace:key }} (supported for body and headers only) + headers: + Authorization: + - "Basic {{ basic-auth:default:token }}" + url: http://flask-api.default.svc.cluster.local/v1/login + method: POST + + # Indicates whether the reconciliation should loop indefinitely. If `rollbackRetriesLimit` is set and the request returns an error, it will stop reconciliation once the limit is reached. + shouldLoopInfinitely: true + # Specifies the duration after which the next reconcile should occur. + nextReconcile: 72h # 3 days + + # waitTimeout: 5m + + # Secrets receiving patches from response data + secretInjectionConfigs: + - secretRef: + name: obtained-token-namespaced + namespace: default + secretKey: token + responsePath: .body.token + # setOwnerReference determines if the secret should be deleted when the associated resource is deleted. + # When injecting multiple keys into the same secret, ensure this field is set consistently for all keys. + setOwnerReference: true + providerConfigRef: + name: http-conf-namespaced diff --git a/examples/namespaced/disposablerequest-with-clusterproviderconfig.yaml b/examples/namespaced/disposablerequest-with-clusterproviderconfig.yaml new file mode 100644 index 0000000..1a6c2a8 --- /dev/null +++ b/examples/namespaced/disposablerequest-with-clusterproviderconfig.yaml @@ -0,0 +1,61 @@ +apiVersion: http.m.crossplane.io/v1alpha2 +kind: DisposableRequest +metadata: + name: send-notification-with-cluster-config + namespace: default +spec: + deletionPolicy: Orphan + forProvider: + # This example demonstrates using ClusterProviderConfig with namespaced DisposableRequest + # ClusterProviderConfig allows cross-namespace access to secrets and configurations + url: http://flask-api.default.svc.cluster.local/v1/notify + method: POST + body: | + { + "recipient": "admin@example.com", + "subject": "Cluster Config Alert", + "message": "Hello from Crossplane namespaced resource with cluster config!", + "user": "{{ admin-user:crossplane-system:username }}" + } + headers: + Content-Type: + - application/json + Authorization: + - "Bearer {{ admin-token:crossplane-system:token }}" + insecureSkipTLSVerify: true + + # The 'expectedResponse' field is optional. If used, also set 'rollbackRetriesLimit', which determines the number of HTTP requests to be sent until the jq query returns true. + expectedResponse: '.body.status == "sent"' + rollbackRetriesLimit: 5 + waitTimeout: 5m + + # Indicates whether the reconciliation should loop indefinitely. If `rollbackRetriesLimit` is set and the request returns an error, it will stop reconciliation once the limit is reached. + # shouldLoopInfinitely: true + + # Specifies the duration after which the next reconcile should occur. + # nextReconcile: 3m + + # Using ClusterProviderConfig allows accessing secrets from crossplane-system namespace + secretInjectionConfigs: + - secretRef: + name: notification-response-cluster + namespace: crossplane-system # Cross-namespace access via ClusterProviderConfig + metadata: + labels: + status: .body.status + managed-by: provider-http-cluster-config + annotations: + key: value + keyMappings: + - secretKey: notification-status + responseJQ: .body.status + missingFieldStrategy: setEmpty + - secretKey: notification-id + responseJQ: .body.id + missingFieldStrategy: preserve + # setOwnerReference determines if the secret should be deleted when the associated resource is deleted. + setOwnerReference: true + + # Using ClusterProviderConfig for cross-namespace access + providerConfigRef: + name: http-conf-cluster \ No newline at end of file diff --git a/examples/namespaced/disposablerequest.yaml b/examples/namespaced/disposablerequest.yaml new file mode 100644 index 0000000..3e4706b --- /dev/null +++ b/examples/namespaced/disposablerequest.yaml @@ -0,0 +1,58 @@ +apiVersion: http.m.crossplane.io/v1alpha2 +kind: DisposableRequest +metadata: + name: send-notification-namespaced + namespace: default +spec: + deletionPolicy: Orphan + forProvider: + # Injecting data from secrets is possible, simply use the following syntax: {{ name:namespace:key }} (supported for body and headers only) + url: http://flask-api.default.svc.cluster.local/v1/notify + method: POST + body: | + { + "recipient": "namespaced-user@example.com", + "subject": "Namespaced Alert", + "message": "Your action is required immediately from namespaced resource." + } + headers: + Content-Type: + - application/json + Authorization: + - "Bearer {{ auth:default:token }}" + insecureSkipTLSVerify: true + + # The 'expectedResponse' field is optional. If used, also set 'rollbackRetriesLimit', which determines the number of HTTP requests to be sent until the jq query returns true. + expectedResponse: '.body.status == "sent"' + rollbackRetriesLimit: 5 + waitTimeout: 5m + + # Indicates whether the reconciliation should loop indefinitely. If `rollbackRetriesLimit` is set and the request returns an error, it will stop reconciliation once the limit is reached. + # shouldLoopInfinitely: true + + # Specifies the duration after which the next reconcile should occur. + # nextReconcile: 3m + + # Secrets receiving patches from response data + secretInjectionConfigs: + - secretRef: + name: notification-response-namespaced + namespace: default + metadata: + labels: + status: .body.status + managed-by: provider-http-namespaced + annotations: + key: value + keyMappings: + - secretKey: notification-status + responseJQ: .body.status + missingFieldStrategy: setEmpty + - secretKey: notification-id + responseJQ: .body.id + missingFieldStrategy: preserve + # setOwnerReference determines if the secret should be deleted when the associated resource is deleted. + setOwnerReference: true + + providerConfigRef: + name: http-conf-namespaced diff --git a/examples/namespaced/providerconfig.yaml b/examples/namespaced/providerconfig.yaml new file mode 100644 index 0000000..7b3208e --- /dev/null +++ b/examples/namespaced/providerconfig.yaml @@ -0,0 +1,8 @@ +apiVersion: http.m.crossplane.io/v1alpha2 +kind: ProviderConfig +metadata: + name: http-conf-namespaced + namespace: default +spec: + credentials: + source: None \ No newline at end of file diff --git a/examples/namespaced/request-with-clusterproviderconfig.yaml b/examples/namespaced/request-with-clusterproviderconfig.yaml new file mode 100644 index 0000000..dc1dd58 --- /dev/null +++ b/examples/namespaced/request-with-clusterproviderconfig.yaml @@ -0,0 +1,118 @@ +apiVersion: http.m.crossplane.io/v1alpha2 +kind: Request +metadata: + name: manage-user-with-cluster-config + namespace: default +spec: + forProvider: + # This example demonstrates using ClusterProviderConfig with namespaced resources + # ClusterProviderConfig allows cross-namespace access while the resource remains namespaced + insecureSkipTLSVerify: true + waitTimeout: 5m + headers: + Content-Type: + - application/json + Authorization: + - "Bearer {{ auth:crossplane-system:token }}" + payload: + baseUrl: http://flask-api.default.svc.cluster.local/v1/users + body: | + { + "username": "cluster_config_user", + "password": "secretdata {{ user-password:crossplane-system:password }}", + "email": "cluster_config_user@example.com", + "age": 35 + } + mappings: + # Scenario 1: Action specified, method not specified (defaults to POST for CREATE) + - action: CREATE + # method: "POST" + body: | + { + username: .payload.body.username, + email: .payload.body.email, + age: .payload.body.age, + password: .payload.body.password + } + url: .payload.baseUrl + headers: + Content-Type: + - application/json + Authorization: + - "Bearer {{ auth:crossplane-system:token }}" + Extra-Header-For-Post: + - extra-value + + # Scenario 2: Action specified, method not specified (defaults to GET for OBSERVE) + - action: OBSERVE + # method: "GET" + url: .payload.baseUrl + "/" + (.response.body.id | tostring) + + # Scenario 3: Method specified, action not specified (PUT implies UPDATE) + - method: "PUT" + # action: UPDATE + body: | + { + email: .payload.body.email, + age: .payload.body.age + } + url: .payload.baseUrl + "/" + (.response.body.id | tostring) + + # Scenario 4: Action specified, method not specified (defaults to DELETE for REMOVE) + - action: REMOVE + # method: "DELETE" + url: .payload.baseUrl + "/" + (.response.body.id | tostring) + + expectedResponseCheck: + type: CUSTOM + logic: | + if .response.body.password == .payload.body.password + and .response.body.age == 35 + and .response.headers."Content-Type" == ["application/json"] + and .response.headers."X-Secret-Header"[0] == "{{ response-secret:crossplane-system:extracted-header-data }}" + then true + else false + end + + isRemovedCheck: + type: CUSTOM + logic: | + if .response.statusCode == 404 + and .response.body.error == "User not found" + then true + else false + end + + # Using ClusterProviderConfig allows accessing secrets in crossplane-system namespace + secretInjectionConfigs: + - secretRef: + name: response-secret-cluster + namespace: crossplane-system # Cross-namespace access via ClusterProviderConfig + metadata: + labels: + managed-by: provider-http-cluster-config + annotations: + username: .body.username + keyMappings: + - secretKey: extracted-user-email + responseJQ: .body.email + missingFieldStrategy: preserve + - secretKey: extracted-header-data + responseJQ: .headers."X-Secret-Header"[0] + missingFieldStrategy: setEmpty + setOwnerReference: true + + - secretRef: + name: response-user-password-cluster + namespace: crossplane-system # Cross-namespace access via ClusterProviderConfig + keyMappings: + - secretKey: extracted-user-password + responseJQ: .body.password + missingFieldStrategy: delete + - secretKey: extracted-user-age + responseJQ: .body.age + + # Using ClusterProviderConfig instead of namespaced ProviderConfig + # This allows cross-namespace access for secrets and configurations + providerConfigRef: + name: http-conf-cluster \ No newline at end of file diff --git a/examples/namespaced/request.yaml b/examples/namespaced/request.yaml new file mode 100644 index 0000000..4fe19a5 --- /dev/null +++ b/examples/namespaced/request.yaml @@ -0,0 +1,128 @@ +apiVersion: http.m.crossplane.io/v1alpha2 +kind: Request +metadata: + name: manage-user-namespaced + namespace: default +spec: + forProvider: + # Injecting data from secrets is possible, simply use the following syntax: {{ name:namespace:key }} (supported for body and headers only) + insecureSkipTLSVerify: true + waitTimeout: 5m + headers: + Content-Type: + - application/json + Authorization: + - "Bearer {{ auth:default:token }}" + payload: + baseUrl: http://flask-api.default.svc.cluster.local/v1/users + body: | + { + "username": "namespaced_user", + "password": "secretdata {{ user-password:default:password }}", + "email": "namespaced_user@example.com", + "age": 25 + } + mappings: + # Scenario 1: Action specified, method not specified (defaults to POST for CREATE) + - action: CREATE + # method: "POST" + body: | + { + username: .payload.body.username, + email: .payload.body.email, + age: .payload.body.age, + password: .payload.body.password + } + url: .payload.baseUrl + headers: + Content-Type: + - application/json + Authorization: + - "Bearer {{ auth:default:token }}" + Extra-Header-For-Post: + - extra-value + + # Scenario 2: Action specified, method not specified (defaults to GET for OBSERVE) + - action: OBSERVE + # method: "GET" + url: .payload.baseUrl + "/" + (.response.body.id | tostring) + # If the ID of the external resource is known in advance, ownership of the resource can be attempted: + # url: (.payload.baseUrl + "/1234567890") + + # Scenario 3: Method specified, action not specified (PUT implies UPDATE) + - method: "PUT" + # action: UPDATE + body: | + { + email: .payload.body.email, + age: .payload.body.age + } + url: .payload.baseUrl + "/" + (.response.body.id | tostring) + + # Scenario 4: Action specified, method not specified (defaults to DELETE for REMOVE) + - action: REMOVE + # method: "DELETE" + url: .payload.baseUrl + "/" + (.response.body.id | tostring) + + # expectedResponseCheck is optional. If not specified or if the type is "DEFAULT", + # the resource is considered up to date if the GET response containes the PUT body. + # If specified, the JQ logic determines if the resource is up to date: + # - If the JQ query is false, a PUT request is sent to update the resource. + # - If true, the resource is considered up to date. + expectedResponseCheck: + type: CUSTOM + logic: | + if .response.body.password == .payload.body.password + and .response.body.age == 25 + and .response.headers."Content-Type" == ["application/json"] + and .response.headers."X-Secret-Header"[0] == "{{ response-secret:default:extracted-header-data }}" + then true + else false + end + + # isRemovedCheck is optional. If not specified or if the type is "DEFAULT", + # the resource is considered removed if the OBSERVE response after REMOVE has 404 status code. + # If specified, the JQ logic determines if the resource is removed: + # - If the JQ query is false, a REMOVE request is sent to remove the resource. + # - If true, the resource is considered up to date. + isRemovedCheck: + type: CUSTOM + logic: | + if .response.statusCode == 404 + and .response.body.error == "User not found" + then true + else false + end + + # Secrets receiving patches from response data + secretInjectionConfigs: + - secretRef: + name: response-secret-namespaced + namespace: default + metadata: + labels: + managed-by: provider-http-namespaced + annotations: + username: .body.username + keyMappings: + - secretKey: extracted-user-email + responseJQ: .body.email + missingFieldStrategy: preserve + - secretKey: extracted-header-data + responseJQ: .headers."X-Secret-Header"[0] + missingFieldStrategy: setEmpty + # setOwnerReference determines if the secret should be deleted when the associated resource is deleted. + setOwnerReference: true + + - secretRef: + name: response-user-password-namespaced + namespace: default + keyMappings: + - secretKey: extracted-user-password + responseJQ: .body.password + missingFieldStrategy: delete + - secretKey: extracted-user-age + responseJQ: .body.age + + providerConfigRef: + name: http-conf-namespaced \ No newline at end of file diff --git a/go.mod b/go.mod index dd9259b..d14b8c7 100644 --- a/go.mod +++ b/go.mod @@ -95,7 +95,7 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.34.1 - k8s.io/apiextensions-apiserver v0.33.0 // indirect + k8s.io/apiextensions-apiserver v0.33.0 k8s.io/component-base v0.33.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect diff --git a/internal/controller/cluster/cluster.go b/internal/controller/cluster/cluster.go new file mode 100644 index 0000000..546a14b --- /dev/null +++ b/internal/controller/cluster/cluster.go @@ -0,0 +1,69 @@ +/* +Copyright 2020 The Crossplane Authors. + +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 cluster + +import ( + "time" + + "github.com/crossplane/crossplane-runtime/v2/pkg/controller" + ctrl "sigs.k8s.io/controller-runtime" + + clusterdisposablerequestv1alpha2 "github.com/crossplane-contrib/provider-http/apis/cluster/disposablerequest/v1alpha2" + clusterrequestv1alpha2 "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + httpv1alpha1 "github.com/crossplane-contrib/provider-http/apis/cluster/v1alpha1" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/config" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/disposablerequest" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request" +) + +// Setup creates all cluster-scoped http controllers with the supplied logger and adds them to +// the supplied manager. +func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error { + for _, setup := range []func(ctrl.Manager, controller.Options, time.Duration) error{ + config.Setup, + disposablerequest.Setup, + request.Setup, + } { + if err := setup(mgr, o, timeout); err != nil { + return err + } + } + return nil +} + +// SetupGated creates all cluster-scoped http controllers with SafeStart capability (controllers start as their CRDs appear) +func SetupGated(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error { + o.Gate.Register(func() { + if err := config.Setup(mgr, o, timeout); err != nil { + panic(err) + } + }, httpv1alpha1.ProviderConfigGroupVersionKind) + + o.Gate.Register(func() { + if err := disposablerequest.Setup(mgr, o, timeout); err != nil { + panic(err) + } + }, clusterdisposablerequestv1alpha2.DisposableRequestGroupVersionKind) + + o.Gate.Register(func() { + if err := request.Setup(mgr, o, timeout); err != nil { + panic(err) + } + }, clusterrequestv1alpha2.RequestGroupVersionKind) + + return nil +} diff --git a/internal/controller/config/config.go b/internal/controller/cluster/config/config.go similarity index 96% rename from internal/controller/config/config.go rename to internal/controller/cluster/config/config.go index bf2c067..2fef102 100644 --- a/internal/controller/config/config.go +++ b/internal/controller/cluster/config/config.go @@ -26,7 +26,7 @@ import ( "github.com/crossplane/crossplane-runtime/v2/pkg/resource" ctrl "sigs.k8s.io/controller-runtime" - "github.com/crossplane-contrib/provider-http/apis/v1alpha1" + "github.com/crossplane-contrib/provider-http/apis/cluster/v1alpha1" ) // Setup adds a controller that reconciles ProviderConfigs by accounting for diff --git a/internal/controller/disposablerequest/disposablerequest.go b/internal/controller/cluster/disposablerequest/disposablerequest.go similarity index 98% rename from internal/controller/disposablerequest/disposablerequest.go rename to internal/controller/cluster/disposablerequest/disposablerequest.go index dae4dac..8ffe012 100644 --- a/internal/controller/disposablerequest/disposablerequest.go +++ b/internal/controller/cluster/disposablerequest/disposablerequest.go @@ -38,8 +38,8 @@ import ( "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" - "github.com/crossplane-contrib/provider-http/apis/disposablerequest/v1alpha2" - apisv1alpha1 "github.com/crossplane-contrib/provider-http/apis/v1alpha1" + "github.com/crossplane-contrib/provider-http/apis/cluster/disposablerequest/v1alpha2" + apisv1alpha1 "github.com/crossplane-contrib/provider-http/apis/cluster/v1alpha1" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane-contrib/provider-http/internal/utils" ) @@ -104,6 +104,14 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E l := c.logger.WithValues("disposableRequest", cr.Name) + // Set default providerConfigRef if not specified + if cr.GetProviderConfigReference() == nil { + cr.SetProviderConfigReference(&xpv1.Reference{ + Name: "default", + }) + l.Debug("No providerConfigRef specified, defaulting to 'default'") + } + pc := &apisv1alpha1.ProviderConfig{} n := types.NamespacedName{Name: cr.GetProviderConfigReference().Name} if err := c.kube.Get(ctx, n, pc); err != nil { diff --git a/internal/controller/disposablerequest/disposablerequest_test.go b/internal/controller/cluster/disposablerequest/disposablerequest_test.go similarity index 99% rename from internal/controller/disposablerequest/disposablerequest_test.go rename to internal/controller/cluster/disposablerequest/disposablerequest_test.go index 6856ca4..5f08df4 100644 --- a/internal/controller/disposablerequest/disposablerequest_test.go +++ b/internal/controller/cluster/disposablerequest/disposablerequest_test.go @@ -22,7 +22,7 @@ import ( "testing" "time" - "github.com/crossplane-contrib/provider-http/apis/disposablerequest/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/disposablerequest/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane-contrib/provider-http/internal/utils" xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" diff --git a/internal/controller/request/observe.go b/internal/controller/cluster/request/observe.go similarity index 96% rename from internal/controller/request/observe.go rename to internal/controller/cluster/request/observe.go index 576058a..c40bba1 100644 --- a/internal/controller/request/observe.go +++ b/internal/controller/cluster/request/observe.go @@ -4,11 +4,11 @@ import ( "context" "net/http" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" - "github.com/crossplane-contrib/provider-http/internal/controller/request/observe" - "github.com/crossplane-contrib/provider-http/internal/controller/request/requestgen" - "github.com/crossplane-contrib/provider-http/internal/controller/request/requestmapping" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/observe" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestgen" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestmapping" datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" "github.com/crossplane-contrib/provider-http/internal/utils" "github.com/pkg/errors" diff --git a/internal/controller/request/observe/is_deleted_check.go b/internal/controller/cluster/request/observe/is_deleted_check.go similarity index 97% rename from internal/controller/request/observe/is_deleted_check.go rename to internal/controller/cluster/request/observe/is_deleted_check.go index 826b07b..a8842db 100644 --- a/internal/controller/request/observe/is_deleted_check.go +++ b/internal/controller/cluster/request/observe/is_deleted_check.go @@ -4,7 +4,7 @@ import ( "context" "net/http" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/pkg/errors" diff --git a/internal/controller/request/observe/is_deleted_check_test.go b/internal/controller/cluster/request/observe/is_deleted_check_test.go similarity index 98% rename from internal/controller/request/observe/is_deleted_check_test.go rename to internal/controller/cluster/request/observe/is_deleted_check_test.go index d85d65e..124bbc7 100644 --- a/internal/controller/request/observe/is_deleted_check_test.go +++ b/internal/controller/cluster/request/observe/is_deleted_check_test.go @@ -5,7 +5,7 @@ import ( "net/http" "testing" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/crossplane/crossplane-runtime/v2/pkg/test" diff --git a/internal/controller/request/observe/is_synced_check.go b/internal/controller/cluster/request/observe/is_synced_check.go similarity index 97% rename from internal/controller/request/observe/is_synced_check.go rename to internal/controller/cluster/request/observe/is_synced_check.go index 82ad797..ace8462 100644 --- a/internal/controller/request/observe/is_synced_check.go +++ b/internal/controller/cluster/request/observe/is_synced_check.go @@ -6,10 +6,10 @@ import ( "net/http" "strings" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" - "github.com/crossplane-contrib/provider-http/internal/controller/request/requestgen" - "github.com/crossplane-contrib/provider-http/internal/controller/request/requestmapping" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestgen" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestmapping" datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" "github.com/crossplane-contrib/provider-http/internal/json" "github.com/crossplane-contrib/provider-http/internal/utils" diff --git a/internal/controller/request/observe/is_synced_check_test.go b/internal/controller/cluster/request/observe/is_synced_check_test.go similarity index 98% rename from internal/controller/request/observe/is_synced_check_test.go rename to internal/controller/cluster/request/observe/is_synced_check_test.go index 62c4ff2..b29a0d8 100644 --- a/internal/controller/request/observe/is_synced_check_test.go +++ b/internal/controller/cluster/request/observe/is_synced_check_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/crossplane/crossplane-runtime/v2/pkg/test" diff --git a/internal/controller/request/observe/jq_check.go b/internal/controller/cluster/request/observe/jq_check.go similarity index 93% rename from internal/controller/request/observe/jq_check.go rename to internal/controller/cluster/request/observe/jq_check.go index 881e9bf..1c867b3 100644 --- a/internal/controller/request/observe/jq_check.go +++ b/internal/controller/cluster/request/observe/jq_check.go @@ -4,10 +4,10 @@ import ( "context" "fmt" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" - "github.com/crossplane-contrib/provider-http/internal/controller/request/requestgen" - "github.com/crossplane-contrib/provider-http/internal/controller/request/responseconverter" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestgen" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/responseconverter" datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" "github.com/crossplane-contrib/provider-http/internal/jq" "github.com/crossplane-contrib/provider-http/internal/utils" diff --git a/internal/controller/request/observe/jq_check_test.go b/internal/controller/cluster/request/observe/jq_check_test.go similarity index 97% rename from internal/controller/request/observe/jq_check_test.go rename to internal/controller/cluster/request/observe/jq_check_test.go index 8a25c59..d44b3f5 100644 --- a/internal/controller/request/observe/jq_check_test.go +++ b/internal/controller/cluster/request/observe/jq_check_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/crossplane/crossplane-runtime/v2/pkg/test" diff --git a/internal/controller/request/observe_test.go b/internal/controller/cluster/request/observe_test.go similarity index 99% rename from internal/controller/request/observe_test.go rename to internal/controller/cluster/request/observe_test.go index a251571..7040ea1 100644 --- a/internal/controller/request/observe_test.go +++ b/internal/controller/cluster/request/observe_test.go @@ -5,11 +5,11 @@ import ( "net/http" "testing" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" - "github.com/crossplane-contrib/provider-http/internal/controller/request/observe" - "github.com/crossplane-contrib/provider-http/internal/controller/request/requestgen" - "github.com/crossplane-contrib/provider-http/internal/controller/request/requestmapping" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/observe" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestgen" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestmapping" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" diff --git a/internal/controller/request/request.go b/internal/controller/cluster/request/request.go similarity index 94% rename from internal/controller/request/request.go rename to internal/controller/cluster/request/request.go index 1ca794b..884cfe6 100644 --- a/internal/controller/request/request.go +++ b/internal/controller/cluster/request/request.go @@ -33,13 +33,13 @@ import ( "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" - apisv1alpha1 "github.com/crossplane-contrib/provider-http/apis/v1alpha1" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + apisv1alpha1 "github.com/crossplane-contrib/provider-http/apis/cluster/v1alpha1" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" - "github.com/crossplane-contrib/provider-http/internal/controller/request/observe" - "github.com/crossplane-contrib/provider-http/internal/controller/request/requestgen" - "github.com/crossplane-contrib/provider-http/internal/controller/request/requestmapping" - "github.com/crossplane-contrib/provider-http/internal/controller/request/statushandler" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/observe" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestgen" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestmapping" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/statushandler" datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" "github.com/crossplane-contrib/provider-http/internal/utils" ) @@ -99,6 +99,14 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E l := c.logger.WithValues("request", cr.Name) + // Set default providerConfigRef if not specified + if cr.GetProviderConfigReference() == nil { + cr.SetProviderConfigReference(&xpv1.Reference{ + Name: "default", + }) + l.Debug("No providerConfigRef specified, defaulting to 'default'") + } + pc := &apisv1alpha1.ProviderConfig{} n := types.NamespacedName{Name: cr.GetProviderConfigReference().Name} if err := c.kube.Get(ctx, n, pc); err != nil { diff --git a/internal/controller/request/request_test.go b/internal/controller/cluster/request/request_test.go similarity index 99% rename from internal/controller/request/request_test.go rename to internal/controller/cluster/request/request_test.go index dbec6cb..bf84333 100644 --- a/internal/controller/request/request_test.go +++ b/internal/controller/cluster/request/request_test.go @@ -7,7 +7,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" diff --git a/internal/controller/request/requestgen/request_generator.go b/internal/controller/cluster/request/requestgen/request_generator.go similarity index 98% rename from internal/controller/request/requestgen/request_generator.go rename to internal/controller/cluster/request/requestgen/request_generator.go index b33999c..bf82b62 100644 --- a/internal/controller/request/requestgen/request_generator.go +++ b/internal/controller/cluster/request/requestgen/request_generator.go @@ -9,9 +9,9 @@ import ( "github.com/pkg/errors" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" - "github.com/crossplane-contrib/provider-http/internal/controller/request/requestprocessing" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestprocessing" datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" json_util "github.com/crossplane-contrib/provider-http/internal/json" "github.com/crossplane-contrib/provider-http/internal/utils" diff --git a/internal/controller/request/requestgen/request_generator_test.go b/internal/controller/cluster/request/requestgen/request_generator_test.go similarity index 99% rename from internal/controller/request/requestgen/request_generator_test.go rename to internal/controller/cluster/request/requestgen/request_generator_test.go index d4dea24..35c35af 100644 --- a/internal/controller/request/requestgen/request_generator_test.go +++ b/internal/controller/cluster/request/requestgen/request_generator_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/internal/controller/request/requestmapping/mapping.go b/internal/controller/cluster/request/requestmapping/mapping.go similarity index 96% rename from internal/controller/request/requestmapping/mapping.go rename to internal/controller/cluster/request/requestmapping/mapping.go index 06a823a..1bc9209 100644 --- a/internal/controller/request/requestmapping/mapping.go +++ b/internal/controller/cluster/request/requestmapping/mapping.go @@ -4,7 +4,7 @@ import ( "fmt" "net/http" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/pkg/errors" ) diff --git a/internal/controller/request/requestmapping/mapping_test.go b/internal/controller/cluster/request/requestmapping/mapping_test.go similarity index 99% rename from internal/controller/request/requestmapping/mapping_test.go rename to internal/controller/cluster/request/requestmapping/mapping_test.go index b2033a4..c41fa8e 100644 --- a/internal/controller/request/requestmapping/mapping_test.go +++ b/internal/controller/cluster/request/requestmapping/mapping_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" diff --git a/internal/controller/request/requestprocessing/request_processing.go b/internal/controller/cluster/request/requestprocessing/request_processing.go similarity index 100% rename from internal/controller/request/requestprocessing/request_processing.go rename to internal/controller/cluster/request/requestprocessing/request_processing.go diff --git a/internal/controller/request/requestprocessing/request_processing_test.go b/internal/controller/cluster/request/requestprocessing/request_processing_test.go similarity index 100% rename from internal/controller/request/requestprocessing/request_processing_test.go rename to internal/controller/cluster/request/requestprocessing/request_processing_test.go diff --git a/internal/controller/request/responseconverter/converter.go b/internal/controller/cluster/request/responseconverter/converter.go similarity index 83% rename from internal/controller/request/responseconverter/converter.go rename to internal/controller/cluster/request/responseconverter/converter.go index cc23745..e1182ae 100644 --- a/internal/controller/request/responseconverter/converter.go +++ b/internal/controller/cluster/request/responseconverter/converter.go @@ -1,7 +1,7 @@ package responseconverter import ( - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" ) diff --git a/internal/controller/request/responseconverter/converter_test.go b/internal/controller/cluster/request/responseconverter/converter_test.go similarity index 94% rename from internal/controller/request/responseconverter/converter_test.go rename to internal/controller/cluster/request/responseconverter/converter_test.go index 48e0b44..09d30e4 100644 --- a/internal/controller/request/responseconverter/converter_test.go +++ b/internal/controller/cluster/request/responseconverter/converter_test.go @@ -3,7 +3,7 @@ package responseconverter import ( "testing" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/google/go-cmp/cmp" ) diff --git a/internal/controller/request/statushandler/status.go b/internal/controller/cluster/request/statushandler/status.go similarity index 97% rename from internal/controller/request/statushandler/status.go rename to internal/controller/cluster/request/statushandler/status.go index 569d628..eb4c254 100644 --- a/internal/controller/request/statushandler/status.go +++ b/internal/controller/cluster/request/statushandler/status.go @@ -6,10 +6,10 @@ import ( "net/http" "strconv" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" - "github.com/crossplane-contrib/provider-http/internal/controller/request/requestgen" - "github.com/crossplane-contrib/provider-http/internal/controller/request/responseconverter" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestgen" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/responseconverter" "github.com/crossplane-contrib/provider-http/internal/utils" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/pkg/errors" diff --git a/internal/controller/request/statushandler/status_test.go b/internal/controller/cluster/request/statushandler/status_test.go similarity index 98% rename from internal/controller/request/statushandler/status_test.go rename to internal/controller/cluster/request/statushandler/status_test.go index e0a5dc2..d342d52 100644 --- a/internal/controller/request/statushandler/status_test.go +++ b/internal/controller/cluster/request/statushandler/status_test.go @@ -6,7 +6,7 @@ import ( "github.com/pkg/errors" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/crossplane/crossplane-runtime/v2/pkg/test" diff --git a/internal/controller/http.go b/internal/controller/http.go index 2bdd220..f65cdbe 100644 --- a/internal/controller/http.go +++ b/internal/controller/http.go @@ -22,18 +22,29 @@ import ( "github.com/crossplane/crossplane-runtime/v2/pkg/controller" ctrl "sigs.k8s.io/controller-runtime" - "github.com/crossplane-contrib/provider-http/internal/controller/config" - disposablerequest "github.com/crossplane-contrib/provider-http/internal/controller/disposablerequest" - request "github.com/crossplane-contrib/provider-http/internal/controller/request" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster" + "github.com/crossplane-contrib/provider-http/internal/controller/namespaced" ) -// Setup creates all http controllers with the supplied logger and adds them to +// Setup creates all http controllers (both cluster and namespaced) with the supplied logger and adds them to // the supplied manager. func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error { for _, setup := range []func(ctrl.Manager, controller.Options, time.Duration) error{ - config.Setup, - disposablerequest.Setup, - request.Setup, + cluster.Setup, + namespaced.Setup, + } { + if err := setup(mgr, o, timeout); err != nil { + return err + } + } + return nil +} + +// SetupGated creates all http controllers with SafeStart capability (controllers start as their CRDs appear) +func SetupGated(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error { + for _, setup := range []func(ctrl.Manager, controller.Options, time.Duration) error{ + cluster.SetupGated, + namespaced.SetupGated, } { if err := setup(mgr, o, timeout); err != nil { return err diff --git a/internal/controller/namespaced/config/clusterconfig.go b/internal/controller/namespaced/config/clusterconfig.go new file mode 100644 index 0000000..70cbc9f --- /dev/null +++ b/internal/controller/namespaced/config/clusterconfig.go @@ -0,0 +1,53 @@ +/* +Copyright 2020 The Crossplane Authors. + +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 config + +import ( + "time" + + "github.com/crossplane/crossplane-runtime/v2/pkg/controller" + "github.com/crossplane/crossplane-runtime/v2/pkg/event" + "github.com/crossplane/crossplane-runtime/v2/pkg/ratelimiter" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/providerconfig" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + ctrl "sigs.k8s.io/controller-runtime" + + "github.com/crossplane-contrib/provider-http/apis/namespaced/v1alpha2" +) + +// SetupCluster adds a controller that reconciles ClusterProviderConfigs by accounting for +// their current usage. +func SetupCluster(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error { + name := providerconfig.ControllerName(v1alpha2.ClusterProviderConfigGroupKind) + + of := resource.ProviderConfigKinds{ + Config: v1alpha2.ClusterProviderConfigGroupVersionKind, + Usage: v1alpha2.ClusterProviderConfigUsageGroupVersionKind, + UsageList: v1alpha2.ClusterProviderConfigUsageListGroupVersionKind, + } + + r := providerconfig.NewReconciler(mgr, of, + providerconfig.WithLogger(o.Logger.WithValues("controller", name)), + providerconfig.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) + + return ctrl.NewControllerManagedBy(mgr). + Named(name). + WithOptions(o.ForControllerRuntime()). + For(&v1alpha2.ClusterProviderConfig{}). + Watches(&v1alpha2.ClusterProviderConfigUsage{}, &resource.EnqueueRequestForProviderConfig{}). + Complete(ratelimiter.NewReconciler(name, r, o.GlobalRateLimiter)) +} \ No newline at end of file diff --git a/internal/controller/namespaced/config/config.go b/internal/controller/namespaced/config/config.go new file mode 100644 index 0000000..4c43116 --- /dev/null +++ b/internal/controller/namespaced/config/config.go @@ -0,0 +1,53 @@ +/* +Copyright 2020 The Crossplane Authors. + +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 config + +import ( + "time" + + "github.com/crossplane/crossplane-runtime/v2/pkg/controller" + "github.com/crossplane/crossplane-runtime/v2/pkg/event" + "github.com/crossplane/crossplane-runtime/v2/pkg/ratelimiter" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/providerconfig" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + ctrl "sigs.k8s.io/controller-runtime" + + "github.com/crossplane-contrib/provider-http/apis/namespaced/v1alpha2" +) + +// Setup adds a controller that reconciles ProviderConfigs by accounting for +// their current usage. +func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error { + name := providerconfig.ControllerName(v1alpha2.ProviderConfigGroupKind) + + of := resource.ProviderConfigKinds{ + Config: v1alpha2.ProviderConfigGroupVersionKind, + Usage: v1alpha2.ProviderConfigUsageGroupVersionKind, + UsageList: v1alpha2.ProviderConfigUsageListGroupVersionKind, + } + + r := providerconfig.NewReconciler(mgr, of, + providerconfig.WithLogger(o.Logger.WithValues("controller", name)), + providerconfig.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) + + return ctrl.NewControllerManagedBy(mgr). + Named(name). + WithOptions(o.ForControllerRuntime()). + For(&v1alpha2.ProviderConfig{}). + Watches(&v1alpha2.ProviderConfigUsage{}, &resource.EnqueueRequestForProviderConfig{}). + Complete(ratelimiter.NewReconciler(name, r, o.GlobalRateLimiter)) +} diff --git a/internal/controller/namespaced/disposablerequest/disposablerequest.go b/internal/controller/namespaced/disposablerequest/disposablerequest.go new file mode 100644 index 0000000..bccfe20 --- /dev/null +++ b/internal/controller/namespaced/disposablerequest/disposablerequest.go @@ -0,0 +1,457 @@ +/* +Copyright 2023 The Crossplane Authors. + +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 disposablerequest + +import ( + "context" + "fmt" + "strconv" + "time" + + datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" + "github.com/crossplane-contrib/provider-http/internal/jq" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + json_util "github.com/crossplane-contrib/provider-http/internal/json" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + "github.com/crossplane/crossplane-runtime/v2/pkg/controller" + "github.com/crossplane/crossplane-runtime/v2/pkg/event" + "github.com/crossplane/crossplane-runtime/v2/pkg/ratelimiter" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + + "github.com/crossplane-contrib/provider-http/apis/namespaced/disposablerequest/v1alpha2" + apisv1alpha2 "github.com/crossplane-contrib/provider-http/apis/namespaced/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/crossplane-contrib/provider-http/internal/utils" +) + +const ( + errNotNamespacedDisposableRequest = "managed resource is not a namespaced DisposableRequest custom resource" + errTrackPCUsage = "cannot track ProviderConfig usage" + errNewHttpClient = "cannot create new Http client" + errProviderNotRetrieved = "provider could not be retrieved" + errFailedToSendHttpDisposableRequest = "failed to send http request" + errFailedUpdateStatusConditions = "failed updating status conditions" + ErrExpectedFormat = "JQ filter should return a boolean, but returned error: %s" + errPatchFromReferencedSecret = "cannot patch from referenced secret" + errGetReferencedSecret = "cannot get referenced secret" + errCreateReferencedSecret = "cannot create referenced secret" + errPatchDataToSecret = "Warning, couldn't patch data from request to secret %s:%s:%s, error: %s" + errConvertResToMap = "failed to convert response to map" + errGetLatestVersion = "failed to get the latest version of the resource" + errResponseFormat = "Response does not match the expected format, retries limit " + errExtractCredentials = "cannot extract credentials" + errCheckExpectedResponse = "failed to check if response is as expected" + errResponseDoesntMatchExpectedCriteria = "response does not match expected criteria" +) + +// Setup adds a controller that reconciles namespaced DisposableRequest managed resources. +func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error { + name := managed.ControllerName(v1alpha2.DisposableRequestGroupKind) + + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha2.DisposableRequestGroupVersionKind), + managed.WithExternalConnecter(&connector{ + logger: o.Logger, + kube: mgr.GetClient(), + newHttpClientFn: httpClient.NewClient, + }), + managed.WithLogger(o.Logger.WithValues("controller", name)), + managed.WithPollInterval(o.PollInterval), + WithCustomPollIntervalHook(), + managed.WithTimeout(timeout), + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) + + return ctrl.NewControllerManagedBy(mgr). + Named(name). + WithOptions(o.ForControllerRuntime()). + WithEventFilter(resource.DesiredStateChanged()). + For(&v1alpha2.DisposableRequest{}). + Complete(ratelimiter.NewReconciler(name, r, o.GlobalRateLimiter)) +} + +type connector struct { + logger logging.Logger + kube client.Client + newHttpClientFn func(log logging.Logger, timeout time.Duration, creds string) (httpClient.Client, error) +} + +// Connect returns a new ExternalClient. +func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { + cr, ok := mg.(*v1alpha2.DisposableRequest) + if !ok { + return nil, errors.New(errNotNamespacedDisposableRequest) + } + + l := c.logger.WithValues("namespacedDisposableRequest", cr.Name) + + // Set default providerConfigRef if not specified + if cr.GetProviderConfigReference() == nil { + cr.SetProviderConfigReference(&xpv1.Reference{ + Name: "default", + }) + l.Debug("No providerConfigRef specified, defaulting to 'default'") + } + + // Get the namespaced ProviderConfig + pc := &apisv1alpha2.ProviderConfig{} + n := types.NamespacedName{Name: cr.GetProviderConfigReference().Name, Namespace: cr.Namespace} + if err := c.kube.Get(ctx, n, pc); err != nil { + return nil, errors.Wrap(err, errProviderNotRetrieved) + } + + creds := "" + if pc.Spec.Credentials.Source == xpv1.CredentialsSourceSecret { + data, err := resource.CommonCredentialExtractor(ctx, pc.Spec.Credentials.Source, c.kube, pc.Spec.Credentials.CommonCredentialSelectors) + if err != nil { + return nil, errors.Wrap(err, errExtractCredentials) + } + + creds = string(data) + } + + h, err := c.newHttpClientFn(l, utils.WaitTimeout(cr.Spec.ForProvider.WaitTimeout), creds) + if err != nil { + return nil, errors.Wrap(err, errNewHttpClient) + } + + return &external{ + localKube: c.kube, + logger: l, + http: h, + }, nil +} + +type external struct { + localKube client.Client + logger logging.Logger + http httpClient.Client +} + +// Observe checks the state of the DisposableRequest resource and updates its status accordingly. +func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { + cr, ok := mg.(*v1alpha2.DisposableRequest) + if !ok { + return managed.ExternalObservation{}, errors.New(errNotNamespacedDisposableRequest) + } + + isUpToDate := !(utils.ShouldRetry(cr.Spec.ForProvider.RollbackRetriesLimit, cr.Status.Failed) && !utils.RetriesLimitReached(cr.Status.Failed, cr.Spec.ForProvider.RollbackRetriesLimit)) + isAvailable := isUpToDate + + if !cr.Status.Synced { + return managed.ExternalObservation{ + ResourceExists: false, + }, nil + } + + isExpected, storedResponse, err := c.validateStoredResponse(ctx, cr) + if err != nil { + return managed.ExternalObservation{}, err + } + if !isExpected { + return managed.ExternalObservation{}, errors.New(errResponseDoesntMatchExpectedCriteria) + } + + isUpToDate = c.calculateUpToDateStatus(cr, isUpToDate) + + if isAvailable { + if err := c.updateResourceStatus(ctx, cr); err != nil { + return managed.ExternalObservation{}, err + } + } + + if len(cr.Spec.ForProvider.SecretInjectionConfigs) > 0 && cr.Status.Response.StatusCode != 0 { + c.applySecretInjectionsFromStoredResponse(ctx, cr, storedResponse, isExpected) + } + + return managed.ExternalObservation{ + ResourceExists: isAvailable, + ResourceUpToDate: isUpToDate, + ConnectionDetails: nil, + }, nil +} + +// validateStoredResponse validates the stored response against expected criteria +func (c *external) validateStoredResponse(ctx context.Context, cr *v1alpha2.DisposableRequest) (bool, httpClient.HttpResponse, error) { + sensitiveBody, err := datapatcher.PatchSecretsIntoString(ctx, c.localKube, cr.Status.Response.Body, c.logger) + if err != nil { + return false, httpClient.HttpResponse{}, errors.Wrap(err, errPatchFromReferencedSecret) + } + + storedResponse := httpClient.HttpResponse{ + StatusCode: cr.Status.Response.StatusCode, + Headers: cr.Status.Response.Headers, + Body: sensitiveBody, + } + + isExpected, err := c.isResponseAsExpected(cr, storedResponse) + if err != nil { + c.logger.Debug("Setting error condition due to validation error", "error", err) + return false, httpClient.HttpResponse{}, errors.Wrap(err, errCheckExpectedResponse) + } + if !isExpected { + c.logger.Debug("Response does not match expected criteria") + return false, httpClient.HttpResponse{}, nil + } + + return true, storedResponse, nil +} + +// calculateUpToDateStatus determines if the resource should be considered up-to-date +func (c *external) calculateUpToDateStatus(cr *v1alpha2.DisposableRequest, currentStatus bool) bool { + // If shouldLoopInfinitely is true, the resource should never be considered up-to-date + if cr.Spec.ForProvider.ShouldLoopInfinitely { + if cr.Spec.ForProvider.RollbackRetriesLimit == nil { + return false + } + } + return currentStatus +} + +// updateResourceStatus updates the resource status to Available +func (c *external) updateResourceStatus(ctx context.Context, cr *v1alpha2.DisposableRequest) error { + if err := c.localKube.Get(ctx, types.NamespacedName{Name: cr.Name, Namespace: cr.Namespace}, cr); err != nil { + return errors.Wrap(err, errGetLatestVersion) + } + + cr.Status.SetConditions(xpv1.Available()) + if err := c.localKube.Status().Update(ctx, cr); err != nil { + return errors.New(errFailedUpdateStatusConditions) + } + return nil +} + +// deployAction sends the HTTP request defined in the DisposableRequest resource and updates its status based on the response. +func (c *external) deployAction(ctx context.Context, cr *v1alpha2.DisposableRequest) error { + if cr.Status.Synced { + c.logger.Debug("Resource is already synced, skipping deployment action") + return nil + } + + // Check if retries limit has been reached + if utils.RollBackEnabled(cr.Spec.ForProvider.RollbackRetriesLimit) && utils.RetriesLimitReached(cr.Status.Failed, cr.Spec.ForProvider.RollbackRetriesLimit) { + c.logger.Debug("Retries limit reached, not retrying anymore") + return nil + } + + details, httpRequestErr := c.sendHttpRequest(ctx, cr) + + resource, err := c.prepareRequestResource(ctx, cr, details) + if err != nil { + return err + } + + // Handle HTTP request errors first + if httpRequestErr != nil { + return c.handleHttpRequestError(ctx, cr, resource, httpRequestErr) + } + + return c.handleHttpResponse(ctx, cr, details.HttpResponse, resource) +} + +// applySecretInjectionsFromStoredResponse applies secret injection configurations using the stored response +// This is used when the resource is already synced but secret injection configs may have been updated +func (c *external) applySecretInjectionsFromStoredResponse(ctx context.Context, cr *v1alpha2.DisposableRequest, storedResponse httpClient.HttpResponse, isExpectedResponse bool) { + if isExpectedResponse { + c.logger.Debug("Applying secret injections from stored response") + datapatcher.ApplyResponseDataToSecrets(ctx, c.localKube, c.logger, &storedResponse, cr.Spec.ForProvider.SecretInjectionConfigs, cr) + return + } + + c.logger.Debug("Skipping secret injections as response does not match expected criteria") +} + +// sendHttpRequest sends the HTTP request with sensitive data patched +func (c *external) sendHttpRequest(ctx context.Context, cr *v1alpha2.DisposableRequest) (httpClient.HttpDetails, error) { + sensitiveBody, err := datapatcher.PatchSecretsIntoString(ctx, c.localKube, cr.Spec.ForProvider.Body, c.logger) + if err != nil { + return httpClient.HttpDetails{}, err + } + + sensitiveHeaders, err := datapatcher.PatchSecretsIntoHeaders(ctx, c.localKube, cr.Spec.ForProvider.Headers, c.logger) + if err != nil { + return httpClient.HttpDetails{}, err + } + + bodyData := httpClient.Data{Encrypted: cr.Spec.ForProvider.Body, Decrypted: sensitiveBody} + headersData := httpClient.Data{Encrypted: cr.Spec.ForProvider.Headers, Decrypted: sensitiveHeaders} + details, err := c.http.SendRequest(ctx, cr.Spec.ForProvider.Method, cr.Spec.ForProvider.URL, bodyData, headersData, cr.Spec.ForProvider.InsecureSkipTLSVerify) + + return details, err +} + +// prepareRequestResource creates and initializes the RequestResource +func (c *external) prepareRequestResource(ctx context.Context, cr *v1alpha2.DisposableRequest, details httpClient.HttpDetails) (*utils.RequestResource, error) { + resource := &utils.RequestResource{ + Resource: cr, + RequestContext: ctx, + HttpResponse: details.HttpResponse, + LocalClient: c.localKube, + HttpRequest: details.HttpRequest, + } + + // Get the latest version of the resource before updating + if err := c.localKube.Get(ctx, types.NamespacedName{Name: cr.Name, Namespace: cr.Namespace}, cr); err != nil { + return nil, errors.Wrap(err, errGetLatestVersion) + } + + return resource, nil +} + +// handleHttpResponse processes the HTTP response and updates resource status accordingly +func (c *external) handleHttpResponse(ctx context.Context, cr *v1alpha2.DisposableRequest, sensitiveResponse httpClient.HttpResponse, resource *utils.RequestResource) error { + // Handle HTTP error status codes + if utils.IsHTTPError(resource.HttpResponse.StatusCode) { + return c.handleHttpErrorStatus(ctx, cr, resource) + } + + // Handle response validation + return c.handleResponseValidation(ctx, cr, sensitiveResponse, resource) +} + +// handleHttpRequestError handles cases where the HTTP request itself failed +func (c *external) handleHttpRequestError(ctx context.Context, cr *v1alpha2.DisposableRequest, resource *utils.RequestResource, httpRequestErr error) error { + setErr := resource.SetError(httpRequestErr) + c.applySecretInjectionsFromStoredResponse(ctx, cr, resource.HttpResponse, false) + if settingError := utils.SetRequestResourceStatus(*resource, setErr, resource.SetLastReconcileTime(), resource.SetRequestDetails()); settingError != nil { + return errors.Wrap(settingError, utils.ErrFailedToSetStatus) + } + return httpRequestErr +} + +// handleHttpErrorStatus handles HTTP error status codes +func (c *external) handleHttpErrorStatus(ctx context.Context, cr *v1alpha2.DisposableRequest, resource *utils.RequestResource) error { + c.applySecretInjectionsFromStoredResponse(ctx, cr, resource.HttpResponse, false) + if settingError := utils.SetRequestResourceStatus(*resource, resource.SetStatusCode(), resource.SetLastReconcileTime(), resource.SetHeaders(), resource.SetBody(), resource.SetRequestDetails(), resource.SetError(nil)); settingError != nil { + return errors.Wrap(settingError, utils.ErrFailedToSetStatus) + } + + return errors.Errorf(utils.ErrStatusCode, cr.Spec.ForProvider.Method, strconv.Itoa(resource.HttpResponse.StatusCode)) +} + +// handleResponseValidation validates the response and updates status accordingly +func (c *external) handleResponseValidation(ctx context.Context, cr *v1alpha2.DisposableRequest, sensitiveResponse httpClient.HttpResponse, resource *utils.RequestResource) error { + isExpectedResponse, err := c.isResponseAsExpected(cr, sensitiveResponse) + if err != nil { + return err + } + + if isExpectedResponse { + c.applySecretInjectionsFromStoredResponse(ctx, cr, resource.HttpResponse, true) + return utils.SetRequestResourceStatus(*resource, resource.SetStatusCode(), resource.SetLastReconcileTime(), resource.SetHeaders(), resource.SetBody(), resource.SetSynced(), resource.SetRequestDetails()) + } + + limit := utils.GetRollbackRetriesLimit(cr.Spec.ForProvider.RollbackRetriesLimit) + return utils.SetRequestResourceStatus(*resource, resource.SetStatusCode(), resource.SetLastReconcileTime(), resource.SetHeaders(), resource.SetBody(), + resource.SetError(errors.New(errResponseFormat+fmt.Sprint(limit))), resource.SetRequestDetails()) +} + +func (c *external) isResponseAsExpected(cr *v1alpha2.DisposableRequest, res httpClient.HttpResponse) (bool, error) { + // If no expected response is defined, consider it as expected. + if cr.Spec.ForProvider.ExpectedResponse == "" { + return true, nil + } + + if res.StatusCode == 0 { + return false, nil + } + + responseMap, err := json_util.StructToMap(res) + if err != nil { + return false, errors.Wrap(err, errConvertResToMap) + } + + json_util.ConvertJSONStringsToMaps(&responseMap) + + isExpected, err := jq.ParseBool(cr.Spec.ForProvider.ExpectedResponse, responseMap) + if err != nil { + return false, errors.Errorf(ErrExpectedFormat, err.Error()) + } + + return isExpected, nil +} + +func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { + cr, ok := mg.(*v1alpha2.DisposableRequest) + if !ok { + return managed.ExternalCreation{}, errors.New(errNotNamespacedDisposableRequest) + } + + if err := utils.IsRequestValid(cr.Spec.ForProvider.Method, cr.Spec.ForProvider.URL); err != nil { + return managed.ExternalCreation{}, err + } + + return managed.ExternalCreation{}, errors.Wrap(c.deployAction(ctx, cr), errFailedToSendHttpDisposableRequest) +} + +func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { + cr, ok := mg.(*v1alpha2.DisposableRequest) + if !ok { + return managed.ExternalUpdate{}, errors.New(errNotNamespacedDisposableRequest) + } + + if err := utils.IsRequestValid(cr.Spec.ForProvider.Method, cr.Spec.ForProvider.URL); err != nil { + return managed.ExternalUpdate{}, err + } + + return managed.ExternalUpdate{}, errors.Wrap(c.deployAction(ctx, cr), errFailedToSendHttpDisposableRequest) +} + +func (c *external) Delete(_ context.Context, _ resource.Managed) (managed.ExternalDelete, error) { + return managed.ExternalDelete{}, nil +} + +// Disconnect does nothing. It never returns an error. +func (c *external) Disconnect(_ context.Context) error { + return nil +} + +// WithCustomPollIntervalHook returns a managed.ReconcilerOption that sets a custom poll interval based on the DisposableRequest spec. +func WithCustomPollIntervalHook() managed.ReconcilerOption { + return managed.WithPollIntervalHook(func(mg resource.Managed, pollInterval time.Duration) time.Duration { + defaultPollInterval := 30 * time.Second + + cr, ok := mg.(*v1alpha2.DisposableRequest) + if !ok { + return defaultPollInterval + } + + if cr.Spec.ForProvider.NextReconcile == nil { + return defaultPollInterval + } + + // Calculate next reconcile time based on NextReconcile duration + nextReconcileDuration := cr.Spec.ForProvider.NextReconcile.Duration + lastReconcileTime := cr.Status.LastReconcileTime.Time + nextReconcileTime := lastReconcileTime.Add(nextReconcileDuration) + + // Determine if the current time is past the next reconcile time + now := time.Now() + if now.Before(nextReconcileTime) { + // If not yet time to reconcile, calculate remaining time + return nextReconcileTime.Sub(now) + } + + // Default poll interval if the next reconcile time is in the past + return defaultPollInterval + }) +} diff --git a/internal/controller/namespaced/namespaced.go b/internal/controller/namespaced/namespaced.go new file mode 100644 index 0000000..0c5f81d --- /dev/null +++ b/internal/controller/namespaced/namespaced.go @@ -0,0 +1,77 @@ +/* +Copyright 2020 The Crossplane Authors. + +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 namespaced + +import ( + "time" + + "github.com/crossplane/crossplane-runtime/v2/pkg/controller" + ctrl "sigs.k8s.io/controller-runtime" + + namespaceddisposablerequestv1alpha2 "github.com/crossplane-contrib/provider-http/apis/namespaced/disposablerequest/v1alpha2" + namespacedrequestv1alpha2 "github.com/crossplane-contrib/provider-http/apis/namespaced/request/v1alpha2" + namespacedv1alpha2 "github.com/crossplane-contrib/provider-http/apis/namespaced/v1alpha2" + "github.com/crossplane-contrib/provider-http/internal/controller/namespaced/config" + "github.com/crossplane-contrib/provider-http/internal/controller/namespaced/disposablerequest" + "github.com/crossplane-contrib/provider-http/internal/controller/namespaced/request" +) + +// Setup creates all namespaced http controllers with the supplied logger and adds them to +// the supplied manager. +func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error { + for _, setup := range []func(ctrl.Manager, controller.Options, time.Duration) error{ + config.Setup, + config.SetupCluster, + disposablerequest.Setup, + request.Setup, + } { + if err := setup(mgr, o, timeout); err != nil { + return err + } + } + return nil +} + +// SetupGated creates all namespaced http controllers with SafeStart capability (controllers start as their CRDs appear) +func SetupGated(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error { + // Register controllers with gate - they'll start when their CRDs are available + o.Gate.Register(func() { + if err := config.Setup(mgr, o, timeout); err != nil { + panic(err) + } + }, namespacedv1alpha2.ProviderConfigGroupVersionKind) + + o.Gate.Register(func() { + if err := config.SetupCluster(mgr, o, timeout); err != nil { + panic(err) + } + }, namespacedv1alpha2.ClusterProviderConfigGroupVersionKind) + + o.Gate.Register(func() { + if err := disposablerequest.Setup(mgr, o, timeout); err != nil { + panic(err) + } + }, namespaceddisposablerequestv1alpha2.DisposableRequestGroupVersionKind) + + o.Gate.Register(func() { + if err := request.Setup(mgr, o, timeout); err != nil { + panic(err) + } + }, namespacedrequestv1alpha2.RequestGroupVersionKind) + + return nil +} diff --git a/internal/controller/namespaced/request/observe.go b/internal/controller/namespaced/request/observe.go new file mode 100644 index 0000000..e4b0c2c --- /dev/null +++ b/internal/controller/namespaced/request/observe.go @@ -0,0 +1,133 @@ +package request + +import ( + "context" + "net/http" + + "github.com/crossplane-contrib/provider-http/apis/namespaced/request/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/observe" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestgen" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestmapping" + "github.com/crossplane-contrib/provider-http/internal/controller/typeconv" + datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" + "github.com/crossplane-contrib/provider-http/internal/utils" + "github.com/pkg/errors" +) + +const ( + errNotValidJSON = "%s is not a valid JSON string: %s" + errConvertResToMap = "failed to convert response to map" + errExpectedResponseCheckType = "%s.Type should be either DEFAULT, CUSTOM or empty" +) + +type ObserveRequestDetails struct { + Details httpClient.HttpDetails + ResponseError error + Synced bool +} + +// NewObserveRequestDetails is a constructor function that initializes +// an instance of ObserveRequestDetails with default values. +func NewObserve(details httpClient.HttpDetails, resErr error, synced bool) ObserveRequestDetails { + return ObserveRequestDetails{ + Synced: synced, + Details: details, + ResponseError: resErr, + } +} + +// NewObserveRequestDetails is a constructor function that initializes +// an instance of ObserveRequestDetails with default values. +func FailedObserve() ObserveRequestDetails { + return ObserveRequestDetails{ + Synced: false, + } +} + +// isUpToDate checks whether desired spec up to date with the observed state for a given request +func (c *external) isUpToDate(ctx context.Context, cr *v1alpha2.Request) (ObserveRequestDetails, error) { + // Convert namespaced request to cluster request for processing + clusterCR, err := typeconv.ToClusterRequest(cr) + if err != nil { + return FailedObserve(), err + } + + mapping, err := requestmapping.GetMapping(&clusterCR.Spec.ForProvider, typeconv.ToClusterRequestActionObserve(), c.logger) + if err != nil { + return FailedObserve(), err + } + + objectNotCreated := !c.isObjectValidForObservation(cr) + + // Evaluate the HTTP request template. If successfully templated, attempt to + // observe the resource. + requestDetails, err := requestgen.GenerateValidRequestDetails(ctx, clusterCR, mapping, c.localKube, c.logger) + if err != nil { + if objectNotCreated { + // The initial request was not successfully templated. Cannot + // confirm existence of the resource, jumping to the default + // behavior of creating before observing. + err = errors.New(observe.ErrObjectNotFound) + } + return FailedObserve(), err + } + + details, responseErr := c.http.SendRequest(ctx, mapping.Method, requestDetails.Url, requestDetails.Body, requestDetails.Headers, cr.Spec.ForProvider.InsecureSkipTLSVerify) + // The initial observation of an object requires a successful HTTP response + // to be considered existing. + if !utils.IsHTTPSuccess(details.HttpResponse.StatusCode) && objectNotCreated { + // Cannot confirm existence of the resource, jumping to the default + // behavior of creating before observing. + return FailedObserve(), errors.New(observe.ErrObjectNotFound) + } + if err := c.determineIfRemoved(ctx, cr, details, responseErr); err != nil { + return FailedObserve(), err + } + + datapatcher.ApplyResponseDataToSecrets(ctx, c.localKube, c.logger, &details.HttpResponse, cr.Spec.ForProvider.SecretInjectionConfigs, cr) + return c.determineIfUpToDate(ctx, cr, details, responseErr) +} + +// determineIfUpToDate determines if the object is up to date based on the response check. +func (c *external) determineIfUpToDate(ctx context.Context, cr *v1alpha2.Request, details httpClient.HttpDetails, responseErr error) (ObserveRequestDetails, error) { + // Convert namespaced request to cluster request for processing + clusterCR, err := typeconv.ToClusterRequest(cr) + if err != nil { + return FailedObserve(), err + } + + responseChecker := observe.GetIsUpToDateResponseCheck(clusterCR, c.localKube, c.logger, c.http) + if responseChecker == nil { + return FailedObserve(), errors.Errorf(errExpectedResponseCheckType, "expectedResponseCheck") + } + + result, err := responseChecker.Check(ctx, clusterCR, details, responseErr) + if err != nil { + return FailedObserve(), err + } + + return NewObserve(details, responseErr, result), nil +} + +// determineIfRemoved determines if the object is removed based on the response check. +func (c *external) determineIfRemoved(ctx context.Context, cr *v1alpha2.Request, details httpClient.HttpDetails, responseErr error) error { + // Convert namespaced request to cluster request for processing + clusterCR, err := typeconv.ToClusterRequest(cr) + if err != nil { + return err + } + + responseChecker := observe.GetIsRemovedResponseCheck(clusterCR, c.localKube, c.logger, c.http) + if responseChecker == nil { + return errors.Errorf(errExpectedResponseCheckType, "isRemovedCheck") + } + + return responseChecker.Check(ctx, clusterCR, details, responseErr) +} + +// isObjectValidForObservation checks if the object is valid for observation +func (c *external) isObjectValidForObservation(cr *v1alpha2.Request) bool { + return cr.Status.Response.StatusCode != 0 && + !(cr.Status.RequestDetails.Method == http.MethodPost && utils.IsHTTPError(cr.Status.Response.StatusCode)) +} diff --git a/internal/controller/namespaced/request/observe/is_deleted_check.go b/internal/controller/namespaced/request/observe/is_deleted_check.go new file mode 100644 index 0000000..a8842db --- /dev/null +++ b/internal/controller/namespaced/request/observe/is_deleted_check.go @@ -0,0 +1,77 @@ +package observe + +import ( + "context" + "net/http" + + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/pkg/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + ErrObjectNotFound = "object wasn't found" +) + +// isDeletedCheck is an interface for performing isDeleted checks. +type isDeletedCheck interface { + Check(ctx context.Context, cr *v1alpha2.Request, details httpClient.HttpDetails, responseErr error) error +} + +// defaultIsRemovedResponseCheck performs a default comparison between the response and desired state. +type defaultIsRemovedResponseCheck struct { + localKube client.Client + logger logging.Logger + http httpClient.Client +} + +// Check performs a default comparison between the response and desired state. +func (d *defaultIsRemovedResponseCheck) Check(ctx context.Context, cr *v1alpha2.Request, details httpClient.HttpDetails, responseErr error) error { + if details.HttpResponse.StatusCode == http.StatusNotFound { + return errors.New(ErrObjectNotFound) + } + + return nil +} + +// // customIsRemovedResponseCheck performs a custom response check using JQ logic. +type customIsRemovedResponseCheck struct { + localKube client.Client + logger logging.Logger + http httpClient.Client +} + +// Check performs a custom response check using JQ logic. +func (c *customIsRemovedResponseCheck) Check(ctx context.Context, cr *v1alpha2.Request, details httpClient.HttpDetails, responseErr error) error { + logic := cr.Spec.ForProvider.IsRemovedCheck.Logic + customCheck := &customCheck{localKube: c.localKube, logger: c.logger, http: c.http} + + isRemoved, err := customCheck.check(ctx, cr, details, logic) + if err != nil { + return errors.Errorf(errExpectedFormat, "isRemovedCheck", err.Error()) + } else if isRemoved { + return errors.New(ErrObjectNotFound) + } + + return nil +} + +// isRemovedCheckFactoryMap is a map that associates each check type with its corresponding factory function. +var isRemovedCheckFactoryMap = map[string]func(localKube client.Client, logger logging.Logger, http httpClient.Client) isDeletedCheck{ + v1alpha2.ExpectedResponseCheckTypeDefault: func(localKube client.Client, logger logging.Logger, http httpClient.Client) isDeletedCheck { + return &defaultIsRemovedResponseCheck{localKube: localKube, logger: logger, http: http} + }, + v1alpha2.ExpectedResponseCheckTypeCustom: func(localKube client.Client, logger logging.Logger, http httpClient.Client) isDeletedCheck { + return &customIsRemovedResponseCheck{localKube: localKube, logger: logger, http: http} + }, +} + +// GetIsRemovedResponseCheck uses a map to select and return the appropriate ResponseCheck. +func GetIsRemovedResponseCheck(cr *v1alpha2.Request, localKube client.Client, logger logging.Logger, http httpClient.Client) isDeletedCheck { + if factory, ok := isRemovedCheckFactoryMap[cr.Spec.ForProvider.IsRemovedCheck.Type]; ok { + return factory(localKube, logger, http) + } + return isRemovedCheckFactoryMap[v1alpha2.ExpectedResponseCheckTypeDefault](localKube, logger, http) +} diff --git a/internal/controller/namespaced/request/observe/is_deleted_check_test.go b/internal/controller/namespaced/request/observe/is_deleted_check_test.go new file mode 100644 index 0000000..124bbc7 --- /dev/null +++ b/internal/controller/namespaced/request/observe/is_deleted_check_test.go @@ -0,0 +1,243 @@ +package observe + +import ( + "context" + "net/http" + "testing" + + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" +) + +func Test_DefaultIsRemovedCheck(t *testing.T) { + type args struct { + ctx context.Context + cr *v1alpha2.Request + details httpClient.HttpDetails + responseErr error + } + + type want struct { + err error + } + + cases := map[string]struct { + args args + want want + }{ + "ValidRemovedState": { + args: args{ + ctx: context.Background(), + cr: &v1alpha2.Request{}, + details: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + Body: ``, + Headers: nil, + StatusCode: http.StatusNotFound, + }, + }, + responseErr: nil, + }, + want: want{ + err: errors.New(ErrObjectNotFound), + }, + }, + "RemovedStateWithValidJSON": { + args: args{ + ctx: context.Background(), + cr: &v1alpha2.Request{ + Spec: v1alpha2.RequestSpec{ + ForProvider: v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testPostMapping, + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + ExpectedResponseCheck: v1alpha2.ExpectedResponseCheck{ + Type: v1alpha2.ExpectedResponseCheckTypeDefault, + }, + }, + }, + }, details: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + Body: `{"username": "john_doe"}`, + Headers: nil, + StatusCode: http.StatusNotFound, + }, + }, + responseErr: nil, + }, + want: want{ + err: errors.New(ErrObjectNotFound), + }, + }, + "ValidNotRemovedState": { + args: args{ + ctx: context.Background(), + cr: &v1alpha2.Request{}, + details: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + Body: ``, + Headers: nil, + StatusCode: http.StatusOK, + }, + }, + responseErr: nil, + }, + want: want{ + err: nil, + }, + }, + } + + for name, tc := range cases { + tc := tc + + t.Run(name, func(t *testing.T) { + e := &defaultIsRemovedResponseCheck{ + localKube: nil, + http: nil, + logger: logging.NewNopLogger(), + } + gotErr := e.Check(tc.args.ctx, tc.args.cr, tc.args.details, tc.args.responseErr) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("Check(...): -want error, +got error: %s", diff) + } + }) + } +} + +func Test_CustomIsRemovedCheck(t *testing.T) { + type args struct { + ctx context.Context + cr *v1alpha2.Request + details httpClient.HttpDetails + responseErr error + } + + type want struct { + err error + } + + cases := map[string]struct { + args args + want want + }{ + "CustomCheckPasses": { + args: args{ + ctx: context.Background(), + cr: &v1alpha2.Request{ + Spec: v1alpha2.RequestSpec{ + ForProvider: v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: `{"password": "password"}`, + }, + ExpectedResponseCheck: v1alpha2.ExpectedResponseCheck{ + Type: v1alpha2.ExpectedResponseCheckTypeCustom, + Logic: `.response.body.password == .payload.body.password`, + }, + IsRemovedCheck: v1alpha2.ExpectedResponseCheck{ + Type: v1alpha2.ExpectedResponseCheckTypeCustom, + Logic: `.response.body.password == .payload.body.password`, + }, + }, + }, + }, + details: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + Body: `{"password":"password"}`, + Headers: nil, + StatusCode: 0, + }, + }, + responseErr: nil, + }, + want: want{ + err: errors.New(ErrObjectNotFound), + }, + }, + "CustomCheckFails": { + args: args{ + ctx: context.Background(), + cr: &v1alpha2.Request{ + Spec: v1alpha2.RequestSpec{ + ForProvider: v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: `{"password": "password"}`, + }, + ExpectedResponseCheck: v1alpha2.ExpectedResponseCheck{ + Type: v1alpha2.ExpectedResponseCheckTypeCustom, + Logic: `.response.body.password == .payload.body.password`, + }, + IsRemovedCheck: v1alpha2.ExpectedResponseCheck{ + Type: v1alpha2.ExpectedResponseCheckTypeCustom, + Logic: `.response.body.password == .payload.body.password`, + }, + }, + }, + }, + details: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + Body: `{"password":"wrong_password"}`, + Headers: nil, + StatusCode: 0, + }, + }, + responseErr: nil, + }, + want: want{ + err: nil, + }, + }, + "FailedParsing": { + args: args{ + ctx: context.Background(), + cr: &v1alpha2.Request{ + Spec: v1alpha2.RequestSpec{ + ForProvider: v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: `{"password": "password"}`, + }, + }, + }, + }, + details: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + Body: `{"password":"wrong_password"}`, + Headers: nil, + StatusCode: 0, + }, + }, + responseErr: nil, + }, + want: want{ + err: errors.Errorf(errExpectedFormat, "isRemovedCheck", "failed to parse given mapping - jq error: missing query (try \".\")"), + }, + }, + } + + for name, tc := range cases { + tc := tc // Create local copies of loop variables + + t.Run(name, func(t *testing.T) { + e := &customIsRemovedResponseCheck{ + localKube: nil, + http: nil, + logger: logging.NewNopLogger(), + } + gotErr := e.Check(tc.args.ctx, tc.args.cr, tc.args.details, tc.args.responseErr) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("Check(...): -want error, +got error: %s", diff) + } + }) + } +} diff --git a/internal/controller/namespaced/request/observe/is_synced_check.go b/internal/controller/namespaced/request/observe/is_synced_check.go new file mode 100644 index 0000000..ace8462 --- /dev/null +++ b/internal/controller/namespaced/request/observe/is_synced_check.go @@ -0,0 +1,166 @@ +package observe + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestgen" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestmapping" + datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" + "github.com/crossplane-contrib/provider-http/internal/json" + "github.com/crossplane-contrib/provider-http/internal/utils" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/pkg/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + errExpectedFormat = "%s.Logic JQ filter should return a boolean, but returned error: %s" + errNotValidJSON = "%s is not a valid JSON string: %s" +) + +// defaultIsUpToDateResponseCheck performs a default comparison between the response and desired state. +type defaultIsUpToDateResponseCheck struct { + localKube client.Client + logger logging.Logger + http httpClient.Client +} + +// Check performs a default comparison between the response and desired state. +func (d *defaultIsUpToDateResponseCheck) Check(ctx context.Context, cr *v1alpha2.Request, details httpClient.HttpDetails, responseErr error) (bool, error) { + desiredState, err := d.desiredState(ctx, cr) + if err != nil { + if isErrorMappingNotFound(err) { + return true, nil + } + return false, err + } + + return d.compareResponseAndDesiredState(ctx, details, desiredState) +} + +// compareResponseAndDesiredState compares the response and desired state to determine if they are in sync. +func (d *defaultIsUpToDateResponseCheck) compareResponseAndDesiredState(ctx context.Context, details httpClient.HttpDetails, desiredState string) (bool, error) { + sensitiveBody, err := d.patchAndValidate(ctx, details.HttpResponse.Body) + if err != nil { + return false, err + } + + sensitiveDesiredState, err := d.patchAndValidate(ctx, desiredState) + if err != nil { + return false, err + } + + synced, err := d.comparePatchedResults(sensitiveBody, sensitiveDesiredState, details.HttpResponse.StatusCode) + if err != nil { + return false, err + } + + return synced, nil +} + +// patchAndValidate patches secrets into a string and validates the result. +func (d *defaultIsUpToDateResponseCheck) patchAndValidate(ctx context.Context, content string) (string, error) { + patched, err := datapatcher.PatchSecretsIntoString(ctx, d.localKube, content, d.logger) + if err != nil { + return "", err + } + + return patched, nil +} + +// comparePatchedResults compares the patched response and desired state to determine if they are in sync. +func (d *defaultIsUpToDateResponseCheck) comparePatchedResults(body, desiredState string, statusCode int) (bool, error) { + // Both are JSON strings + if json.IsJSONString(body) && json.IsJSONString(desiredState) { + return d.compareJSON(body, desiredState, statusCode), nil + } + + // Body is not JSON but desired state is JSON + if !json.IsJSONString(body) && json.IsJSONString(desiredState) { + return false, errors.Errorf(errNotValidJSON, "response body", body) + } + + // Body is JSON but desired state is not JSON + if json.IsJSONString(body) && !json.IsJSONString(desiredState) { + return false, errors.Errorf(errNotValidJSON, "PUT mapping result", desiredState) + } + + // Compare as strings if neither are JSON + return strings.Contains(body, desiredState) && utils.IsHTTPSuccess(statusCode), nil +} + +// compareJSON compares two JSON strings to determine if they are in sync. +func (d *defaultIsUpToDateResponseCheck) compareJSON(body, desiredState string, statusCode int) bool { + responseBodyMap := json.JsonStringToMap(body) + desiredStateMap := json.JsonStringToMap(desiredState) + return json.Contains(responseBodyMap, desiredStateMap) && utils.IsHTTPSuccess(statusCode) +} + +// desiredState returns the desired state for a given request +func (d *defaultIsUpToDateResponseCheck) desiredState(ctx context.Context, cr *v1alpha2.Request) (string, error) { + requestDetails, err := d.requestDetails(ctx, cr, v1alpha2.ActionUpdate) + if err != nil { + return "", err + } + + return requestDetails.Body.Encrypted.(string), nil +} + +// customIsUpToDateResponseCheck performs a custom response check using JQ logic. +type customIsUpToDateResponseCheck struct { + localKube client.Client + logger logging.Logger + http httpClient.Client +} + +// Check performs a custom response check using JQ logic. +func (c *customIsUpToDateResponseCheck) Check(ctx context.Context, cr *v1alpha2.Request, details httpClient.HttpDetails, responseErr error) (bool, error) { + logic := cr.Spec.ForProvider.ExpectedResponseCheck.Logic + customCheck := &customCheck{localKube: c.localKube, logger: c.logger, http: c.http} + + isUpToDate, err := customCheck.check(ctx, cr, details, logic) + if err != nil { + return false, errors.Errorf(errExpectedFormat, "expectedResponseCheck", err.Error()) + } + + return isUpToDate, nil +} + +// isErrorMappingNotFound checks if the provided error indicates that the +// mapping for an HTTP PUT request is not found. +func isErrorMappingNotFound(err error) bool { + return errors.Cause(err).Error() == fmt.Sprintf(requestmapping.ErrMappingNotFound, v1alpha2.ActionUpdate, http.MethodPut) +} + +// requestDetails generates the request details for a given method or action. +func (d *defaultIsUpToDateResponseCheck) requestDetails(ctx context.Context, cr *v1alpha2.Request, action string) (requestgen.RequestDetails, error) { + mapping, err := requestmapping.GetMapping(&cr.Spec.ForProvider, action, d.logger) + if err != nil { + return requestgen.RequestDetails{}, err + } + + return requestgen.GenerateValidRequestDetails(ctx, cr, mapping, d.localKube, d.logger) +} + +// isUpToDateChecksFactoryMap is a map that associates each check type with its corresponding factory function. +var isUpToDateChecksFactoryMap = map[string]func(localKube client.Client, logger logging.Logger, http httpClient.Client) responseCheck{ + v1alpha2.ExpectedResponseCheckTypeDefault: func(localKube client.Client, logger logging.Logger, http httpClient.Client) responseCheck { + return &defaultIsUpToDateResponseCheck{localKube: localKube, logger: logger, http: http} + }, + v1alpha2.ExpectedResponseCheckTypeCustom: func(localKube client.Client, logger logging.Logger, http httpClient.Client) responseCheck { + return &customIsUpToDateResponseCheck{localKube: localKube, logger: logger, http: http} + }, +} + +// GetIsUpToDateResponseCheck uses a map to select and return the appropriate ResponseCheck. +func GetIsUpToDateResponseCheck(cr *v1alpha2.Request, localKube client.Client, logger logging.Logger, http httpClient.Client) responseCheck { + if factory, ok := isUpToDateChecksFactoryMap[cr.Spec.ForProvider.ExpectedResponseCheck.Type]; ok { + return factory(localKube, logger, http) + } + return isUpToDateChecksFactoryMap[v1alpha2.ExpectedResponseCheckTypeDefault](localKube, logger, http) +} diff --git a/internal/controller/namespaced/request/observe/is_synced_check_test.go b/internal/controller/namespaced/request/observe/is_synced_check_test.go new file mode 100644 index 0000000..b29a0d8 --- /dev/null +++ b/internal/controller/namespaced/request/observe/is_synced_check_test.go @@ -0,0 +1,266 @@ +package observe + +import ( + "context" + "testing" + + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" +) + +var ( + testPostMapping = v1alpha2.Mapping{ + Method: "POST", + Body: "{ username: .payload.body.username, email: .payload.body.email }", + URL: ".payload.baseUrl", + } + + testPutMapping = v1alpha2.Mapping{ + Method: "PUT", + Body: "{ username: \"john_doe_new_username\" }", + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } + + testGetMapping = v1alpha2.Mapping{ + Method: "GET", + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } + + testDeleteMapping = v1alpha2.Mapping{ + Method: "DELETE", + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } +) + +func Test_DefaultIsUpToDateCheck(t *testing.T) { + type args struct { + ctx context.Context + cr *v1alpha2.Request + details httpClient.HttpDetails + responseErr error + } + + type want struct { + result bool + err error + } + + cases := map[string]struct { + args args + want want + }{ + "ValidJSONSyncedState": { + args: args{ + ctx: context.Background(), + cr: &v1alpha2.Request{}, + details: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + Body: ``, + Headers: nil, + StatusCode: 0, + }, + }, + responseErr: nil, + }, + want: want{ + result: true, + err: nil, + }, + }, + "UnsyncedStateWithValidJSON": { + args: args{ + ctx: context.Background(), + cr: &v1alpha2.Request{ + Spec: v1alpha2.RequestSpec{ + ForProvider: v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testPostMapping, + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + ExpectedResponseCheck: v1alpha2.ExpectedResponseCheck{ + Type: v1alpha2.ExpectedResponseCheckTypeDefault, + }, + }, + }, + }, details: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + Body: `{"username": "john_doe"}`, + Headers: nil, + StatusCode: 0, + }, + }, + responseErr: nil, + }, + want: want{ + result: false, + err: nil, + }, + }, + "InvalidResponseJSON": { + args: args{ + ctx: context.Background(), + cr: &v1alpha2.Request{ + Spec: v1alpha2.RequestSpec{ + ForProvider: v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testPostMapping, + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + ExpectedResponseCheck: v1alpha2.ExpectedResponseCheck{ + Type: v1alpha2.ExpectedResponseCheckTypeDefault, + }, + }, + }, + }, + details: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + Body: `{`, + Headers: nil, + StatusCode: 200, + }, + }, + responseErr: nil, + }, + want: want{ + result: false, + err: errors.New("response body is not a valid JSON string: {"), + }, + }, + } + + for name, tc := range cases { + tc := tc + + t.Run(name, func(t *testing.T) { + e := &defaultIsUpToDateResponseCheck{ + localKube: nil, + http: nil, + logger: logging.NewNopLogger(), + } + got, gotErr := e.Check(tc.args.ctx, tc.args.cr, tc.args.details, tc.args.responseErr) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("Check(...): -want error, +got error: %s", diff) + } + + if diff := cmp.Diff(tc.want.result, got); diff != "" { + t.Fatalf("Check(...): -want result, +got result: %s", diff) + } + }) + } +} + +func Test_CustomIsUpToDateCheck(t *testing.T) { + type args struct { + ctx context.Context + cr *v1alpha2.Request + details httpClient.HttpDetails + responseErr error + } + + type want struct { + result bool + err error + } + + cases := map[string]struct { + args args + want want + }{ + "CustomCheckPasses": { + args: args{ + ctx: context.Background(), + cr: &v1alpha2.Request{ + Spec: v1alpha2.RequestSpec{ + ForProvider: v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: `{"password": "password"}`, + }, + ExpectedResponseCheck: v1alpha2.ExpectedResponseCheck{ + Type: v1alpha2.ExpectedResponseCheckTypeCustom, + Logic: `.response.body.password == .payload.body.password`, + }, + }, + }, + }, + details: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + Body: `{"password":"password"}`, + Headers: nil, + StatusCode: 0, + }, + }, + responseErr: nil, + }, + want: want{ + result: true, + err: nil, + }, + }, + "CustomCheckFails": { + args: args{ + ctx: context.Background(), + cr: &v1alpha2.Request{ + Spec: v1alpha2.RequestSpec{ + ForProvider: v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: `{"password": "password"}`, + }, + ExpectedResponseCheck: v1alpha2.ExpectedResponseCheck{ + Type: v1alpha2.ExpectedResponseCheckTypeCustom, + Logic: `.response.body.password == .payload.body.password`, + }, + }, + }, + }, + details: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + Body: `{"password":"wrong_password"}`, + Headers: nil, + StatusCode: 0, + }, + }, + responseErr: nil, + }, + want: want{ + result: false, + err: nil, + }, + }, + } + + for name, tc := range cases { + tc := tc // Create local copies of loop variables + + t.Run(name, func(t *testing.T) { + e := &customIsUpToDateResponseCheck{ + localKube: nil, + http: nil, + logger: logging.NewNopLogger(), + } + got, gotErr := e.Check(tc.args.ctx, tc.args.cr, tc.args.details, tc.args.responseErr) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("Check(...): -want error, +got error: %s", diff) + } + + if diff := cmp.Diff(tc.want.result, got); diff != "" { + t.Fatalf("Check(...): -want result, +got result: %s", diff) + } + }) + } +} diff --git a/internal/controller/namespaced/request/observe/jq_check.go b/internal/controller/namespaced/request/observe/jq_check.go new file mode 100644 index 0000000..1c867b3 --- /dev/null +++ b/internal/controller/namespaced/request/observe/jq_check.go @@ -0,0 +1,56 @@ +package observe + +import ( + "context" + "fmt" + + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestgen" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/responseconverter" + datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" + "github.com/crossplane-contrib/provider-http/internal/jq" + "github.com/crossplane-contrib/provider-http/internal/utils" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// responseCheck is an interface for performing response checks. +type responseCheck interface { + Check(ctx context.Context, cr *v1alpha2.Request, details httpClient.HttpDetails, responseErr error) (bool, error) +} + +// customCheck performs a custom response check using JQ logic. +type customCheck struct { + localKube client.Client + logger logging.Logger + http httpClient.Client +} + +// Check performs a custom response check using JQ logic. +func (c *customCheck) check(ctx context.Context, cr *v1alpha2.Request, details httpClient.HttpDetails, logic string) (bool, error) { + // Convert response to a map and apply JQ logic + response := responseconverter.HttpResponseToV1alpha1Response(details.HttpResponse) + + sensitiveResponse, err := datapatcher.PatchSecretsIntoResponse(ctx, c.localKube, response, c.logger) + if err != nil { + return false, err + } + + sensitiveRequestContext := requestgen.GenerateRequestContext(cr.Spec.ForProvider, sensitiveResponse) + + jqQuery := utils.NormalizeWhitespace(logic) + sensitiveJQQuery, err := datapatcher.PatchSecretsIntoString(ctx, c.localKube, jqQuery, c.logger) + if err != nil { + return false, err + } + + isExpected, err := jq.ParseBool(sensitiveJQQuery, sensitiveRequestContext) + + c.logger.Debug(fmt.Sprintf("Applying JQ filter %s, result is %v", jqQuery, isExpected)) + if err != nil { + return false, err + } + + return isExpected, nil +} diff --git a/internal/controller/namespaced/request/observe/jq_check_test.go b/internal/controller/namespaced/request/observe/jq_check_test.go new file mode 100644 index 0000000..d44b3f5 --- /dev/null +++ b/internal/controller/namespaced/request/observe/jq_check_test.go @@ -0,0 +1,116 @@ +package observe + +import ( + "context" + "testing" + + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" + "github.com/google/go-cmp/cmp" +) + +func Test_CustomCheck(t *testing.T) { + type args struct { + ctx context.Context + cr *v1alpha2.Request + details httpClient.HttpDetails + logic string + } + + type want struct { + result bool + err error + } + + cases := map[string]struct { + args args + want want + }{ + "CustomCheckPasses": { + args: args{ + ctx: context.Background(), + cr: &v1alpha2.Request{ + Spec: v1alpha2.RequestSpec{ + ForProvider: v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: `{"password": "password"}`, + }, + ExpectedResponseCheck: v1alpha2.ExpectedResponseCheck{ + Type: v1alpha2.ExpectedResponseCheckTypeCustom, + Logic: `.response.body.password == .payload.body.password`, + }, + }, + }, + }, + details: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + Body: `{"password":"password"}`, + Headers: nil, + StatusCode: 0, + }, + }, + logic: `.response.body.password == .payload.body.password`, + }, + want: want{ + result: true, + err: nil, + }, + }, + "CustomCheckFails": { + args: args{ + ctx: context.Background(), + cr: &v1alpha2.Request{ + Spec: v1alpha2.RequestSpec{ + ForProvider: v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: `{"password": "password"}`, + }, + ExpectedResponseCheck: v1alpha2.ExpectedResponseCheck{ + Type: v1alpha2.ExpectedResponseCheckTypeCustom, + Logic: `.response.body.password == .payload.body.password`, + }, + IsRemovedCheck: v1alpha2.ExpectedResponseCheck{ + Type: v1alpha2.ExpectedResponseCheckTypeCustom, + Logic: `.response.body.password == .payload.body.password`, + }, + }, + }, + }, + details: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + Body: `{"password":"wrong_password"}`, + Headers: nil, + StatusCode: 0, + }, + }, + logic: `.response.body.password == .payload.body.password`, + }, + want: want{ + result: false, + err: nil, + }, + }, + } + + for name, tc := range cases { + tc := tc // Create local copies of loop variables + + t.Run(name, func(t *testing.T) { + e := &customCheck{ + localKube: nil, + http: nil, + logger: logging.NewNopLogger(), + } + got, gotErr := e.check(tc.args.ctx, tc.args.cr, tc.args.details, tc.args.logic) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("Check(...): -want error, +got error: %s", diff) + } + + if diff := cmp.Diff(tc.want.result, got); diff != "" { + t.Fatalf("Check(...): -want result, +got result: %s", diff) + } + }) + } +} diff --git a/internal/controller/namespaced/request/request.go b/internal/controller/namespaced/request/request.go new file mode 100644 index 0000000..3f18770 --- /dev/null +++ b/internal/controller/namespaced/request/request.go @@ -0,0 +1,250 @@ +/* +Copyright 2024 The Crossplane Authors. + +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 request + +import ( + "context" + "time" + + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + "github.com/crossplane/crossplane-runtime/v2/pkg/controller" + "github.com/crossplane/crossplane-runtime/v2/pkg/event" + "github.com/crossplane/crossplane-runtime/v2/pkg/ratelimiter" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + + "github.com/crossplane-contrib/provider-http/apis/namespaced/request/v1alpha2" + apisv1alpha2 "github.com/crossplane-contrib/provider-http/apis/namespaced/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/crossplane-contrib/provider-http/internal/controller/namespaced/request/observe" + "github.com/crossplane-contrib/provider-http/internal/controller/namespaced/request/requestgen" + "github.com/crossplane-contrib/provider-http/internal/controller/namespaced/request/requestmapping" + "github.com/crossplane-contrib/provider-http/internal/controller/namespaced/request/statushandler" + "github.com/crossplane-contrib/provider-http/internal/controller/typeconv" + datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" + "github.com/crossplane-contrib/provider-http/internal/utils" +) + +const ( + errNotRequest = "managed resource is not a namespaced Request custom resource" + errTrackPCUsage = "cannot track ProviderConfig usage" + errNewHttpClient = "cannot create new Http client" + errProviderNotRetrieved = "provider could not be retrieved" + errFailedToSendHttpRequest = "something went wrong" + errFailedToCheckIfUpToDate = "failed to check if request is up to date" + errFailedToUpdateStatusFailures = "failed to reset status failures counter" + errFailedUpdateStatusConditions = "failed updating status conditions" + errPatchDataToSecret = "Warning, couldn't patch data from request to secret %s:%s:%s, error: %s" + errGetLatestVersion = "failed to get the latest version of the resource" + errExtractCredentials = "cannot extract credentials" +) + +// Setup adds a controller that reconciles namespaced Request managed resources. +func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error { + name := managed.ControllerName(v1alpha2.RequestGroupKind) + + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha2.RequestGroupVersionKind), + managed.WithExternalConnecter(&connector{ + logger: o.Logger, + kube: mgr.GetClient(), + newHttpClientFn: httpClient.NewClient, + }), + managed.WithLogger(o.Logger.WithValues("controller", name)), + managed.WithPollInterval(o.PollInterval), + managed.WithTimeout(timeout), + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) + + return ctrl.NewControllerManagedBy(mgr). + Named(name). + WithOptions(o.ForControllerRuntime()). + WithEventFilter(resource.DesiredStateChanged()). + For(&v1alpha2.Request{}). + Complete(ratelimiter.NewReconciler(name, r, o.GlobalRateLimiter)) +} + +type connector struct { + logger logging.Logger + kube client.Client + newHttpClientFn func(log logging.Logger, timeout time.Duration, creds string) (httpClient.Client, error) +} + +func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { + cr, ok := mg.(*v1alpha2.Request) + if !ok { + return nil, errors.New(errNotRequest) + } + + l := c.logger.WithValues("namespacedRequest", cr.Name) + + // Set default providerConfigRef if not specified + if cr.GetProviderConfigReference() == nil { + cr.SetProviderConfigReference(&xpv1.Reference{ + Name: "default", + }) + l.Debug("No providerConfigRef specified, defaulting to 'default'") + } + + // Get the namespaced ProviderConfig + pc := &apisv1alpha2.ProviderConfig{} + n := types.NamespacedName{Name: cr.GetProviderConfigReference().Name, Namespace: cr.Namespace} + if err := c.kube.Get(ctx, n, pc); err != nil { + return nil, errors.Wrap(err, errProviderNotRetrieved) + } + + creds := "" + if pc.Spec.Credentials.Source == xpv1.CredentialsSourceSecret { + data, err := resource.CommonCredentialExtractor(ctx, pc.Spec.Credentials.Source, c.kube, pc.Spec.Credentials.CommonCredentialSelectors) + if err != nil { + return nil, errors.Wrap(err, errExtractCredentials) + } + + creds = string(data) + } + + h, err := c.newHttpClientFn(l, utils.WaitTimeout(cr.Spec.ForProvider.WaitTimeout), creds) + if err != nil { + return nil, errors.Wrap(err, errNewHttpClient) + } + + return &external{ + localKube: c.kube, + logger: l, + http: h, + }, nil +} + +type external struct { + localKube client.Client + logger logging.Logger + http httpClient.Client +} + +func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { + cr, ok := mg.(*v1alpha2.Request) + if !ok { + return managed.ExternalObservation{}, errors.New(errNotRequest) + } + + observeRequestDetails, err := c.isUpToDate(ctx, cr) + if err != nil && err.Error() == observe.ErrObjectNotFound { + return managed.ExternalObservation{ + ResourceExists: false, + }, nil + } + + if err != nil { + return managed.ExternalObservation{}, errors.Wrap(err, errFailedToCheckIfUpToDate) + } + + // Get the latest version of the resource before updating + if err := c.localKube.Get(ctx, types.NamespacedName{Name: cr.Name, Namespace: cr.Namespace}, cr); err != nil { + return managed.ExternalObservation{}, errors.Wrap(err, errGetLatestVersion) + } + + // Use namespaced statushandler directly + statusHandler, err := statushandler.NewStatusHandler(ctx, cr, observeRequestDetails.Details, observeRequestDetails.ResponseError, c.localKube, c.logger) + if err != nil { + return managed.ExternalObservation{}, err + } + + synced := observeRequestDetails.Synced + if synced { + statusHandler.ResetFailures() + } + + cr.Status.SetConditions(xpv1.Available()) + err = statusHandler.SetRequestStatus() + if err != nil { + return managed.ExternalObservation{}, errors.Wrap(err, " failed updating status") + } + + return managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: synced, + ConnectionDetails: nil, + }, nil +} + +func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { + cr, ok := mg.(*v1alpha2.Request) + if !ok { + return managed.ExternalCreation{}, errors.New(errNotRequest) + } + + return managed.ExternalCreation{}, errors.Wrap(c.deployAction(ctx, cr, v1alpha2.ActionCreate), errFailedToSendHttpRequest) +} + +func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { + cr, ok := mg.(*v1alpha2.Request) + if !ok { + return managed.ExternalUpdate{}, errors.New(errNotRequest) + } + + return managed.ExternalUpdate{}, errors.Wrap(c.deployAction(ctx, cr, v1alpha2.ActionUpdate), errFailedToSendHttpRequest) +} + +func (c *external) Delete(ctx context.Context, mg resource.Managed) (managed.ExternalDelete, error) { + cr, ok := mg.(*v1alpha2.Request) + if !ok { + return managed.ExternalDelete{}, errors.New(errNotRequest) + } + + return managed.ExternalDelete{}, errors.Wrap(c.deployAction(ctx, cr, v1alpha2.ActionRemove), errFailedToSendHttpRequest) +} + +// deployAction executes the action based on the given Request resource and Mapping configuration. +func (c *external) deployAction(ctx context.Context, cr *v1alpha2.Request, action string) error { + // Convert namespaced types for requestmapping and requestgen compatibility + clusterForProvider := typeconv.ConvertNamespacedToClusterRequestParameters(&cr.Spec.ForProvider) + mapping, err := requestmapping.GetMapping(clusterForProvider, action, c.logger) + if err != nil { + c.logger.Info(err.Error()) + return nil + } + + // Convert namespaced request to cluster request for requestgen + clusterCR, err := typeconv.ToClusterRequest(cr) + if err != nil { + return err + } + + requestDetails, err := requestgen.GenerateValidRequestDetails(ctx, clusterCR, mapping, c.localKube, c.logger) + if err != nil { + return err + } + + details, err := c.http.SendRequest(ctx, mapping.Method, requestDetails.Url, requestDetails.Body, requestDetails.Headers, cr.Spec.ForProvider.InsecureSkipTLSVerify) + datapatcher.ApplyResponseDataToSecrets(ctx, c.localKube, c.logger, &details.HttpResponse, cr.Spec.ForProvider.SecretInjectionConfigs, cr) + + statusHandler, err := statushandler.NewStatusHandler(ctx, cr, details, err, c.localKube, c.logger) + if err != nil { + return err + } + + return statusHandler.SetRequestStatus() +} + +func (c *external) Disconnect(_ context.Context) error { + return nil +} diff --git a/internal/controller/namespaced/request/requestgen/request_generator.go b/internal/controller/namespaced/request/requestgen/request_generator.go new file mode 100644 index 0000000..bf82b62 --- /dev/null +++ b/internal/controller/namespaced/request/requestgen/request_generator.go @@ -0,0 +1,156 @@ +package requestgen + +import ( + "context" + "fmt" + "strings" + + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/pkg/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestprocessing" + datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" + json_util "github.com/crossplane-contrib/provider-http/internal/json" + "github.com/crossplane-contrib/provider-http/internal/utils" + + "golang.org/x/exp/maps" +) + +type RequestDetails struct { + Url string + Body httpClient.Data + Headers httpClient.Data +} + +// GenerateRequestDetails generates request details. +func GenerateRequestDetails(ctx context.Context, localKube client.Client, methodMapping v1alpha2.Mapping, forProvider v1alpha2.RequestParameters, response v1alpha2.Response, logger logging.Logger) (RequestDetails, error, bool) { + patchedResponse, err := datapatcher.PatchSecretsIntoResponse(ctx, localKube, response, logger) + if err != nil { + return RequestDetails{}, err, false + } + + jqObject := GenerateRequestContext(forProvider, patchedResponse) + url, err := generateURL(methodMapping.URL, jqObject) + if err != nil { + return RequestDetails{}, err, false + } + + if !utils.IsUrlValid(url) { + return RequestDetails{}, errors.Errorf(utils.ErrInvalidURL, url), false + } + + bodyData, err := generateBody(ctx, localKube, methodMapping.Body, jqObject, logger) + if err != nil { + return RequestDetails{}, err, false + } + + headersData, err := generateHeaders(ctx, localKube, coalesceHeaders(methodMapping.Headers, forProvider.Headers), jqObject, logger) + if err != nil { + return RequestDetails{}, err, false + } + + return RequestDetails{Body: bodyData, Url: url, Headers: headersData}, nil, true +} + +// GenerateRequestContext creates a JSON-compatible map from the specified Request's ForProvider and Response fields. +// It merges the two maps, converts JSON strings to nested maps, and returns the resulting map. +func GenerateRequestContext(forProvider v1alpha2.RequestParameters, patchedResponse v1alpha2.Response) map[string]interface{} { + baseMap, _ := json_util.StructToMap(forProvider) + statusMap, _ := json_util.StructToMap(map[string]interface{}{ + "response": patchedResponse, + }) + + maps.Copy(baseMap, statusMap) + json_util.ConvertJSONStringsToMaps(&baseMap) + + return baseMap +} + +// GenerateValidRequestDetails generates valid request details based on the given Request resource and Mapping configuration. +// It first attempts to generate request details using the HTTP response stored in the Request's status. If the generated +// details are valid, the function returns them. If not, it falls back to using the cached response in the Request's status +// and attempts to generate request details again. The function returns the generated request details or an error if the +// generation process fails. +func GenerateValidRequestDetails(ctx context.Context, cr *v1alpha2.Request, mapping *v1alpha2.Mapping, localKube client.Client, logger logging.Logger) (RequestDetails, error) { + requestDetails, _, ok := GenerateRequestDetails(ctx, localKube, *mapping, cr.Spec.ForProvider, cr.Status.Response, logger) + if IsRequestValid(requestDetails) && ok { + return requestDetails, nil + } + + requestDetails, err, _ := GenerateRequestDetails(ctx, localKube, *mapping, cr.Spec.ForProvider, cr.Status.Cache.Response, logger) + if err != nil { + return RequestDetails{}, err + } + + return requestDetails, nil +} + +// IsRequestValid checks if the request details are valid. +func IsRequestValid(requestDetails RequestDetails) bool { + return (!strings.Contains(fmt.Sprint(requestDetails), "null")) && (requestDetails.Url != "") +} + +// coalesceHeaders returns the non-nil headers, or the default headers if both are nil. +func coalesceHeaders(mappingHeaders, defaultHeaders map[string][]string) map[string][]string { + if mappingHeaders != nil { + return mappingHeaders + } + return defaultHeaders +} + +// generateURL applies a JQ filter to generate a URL. +func generateURL(urlJQFilter string, jqObject map[string]interface{}) (string, error) { + getURL, err := requestprocessing.ApplyJQOnStr(urlJQFilter, jqObject) + if err != nil { + return "", err + } + + return getURL, nil +} + +// generateBody applies a mapping body to generate the request body. +func generateBody(ctx context.Context, localKube client.Client, mappingBody string, jqObject map[string]interface{}, logger logging.Logger) (httpClient.Data, error) { + if mappingBody == "" { + return httpClient.Data{ + Encrypted: "", + Decrypted: "", + }, nil + } + + jqQuery := utils.NormalizeWhitespace(mappingBody) + body, err := requestprocessing.ApplyJQOnStr(jqQuery, jqObject) + if err != nil { + return httpClient.Data{}, err + } + + sensitiveBody, err := datapatcher.PatchSecretsIntoString(ctx, localKube, body, logger) + if err != nil { + return httpClient.Data{}, err + } + + return httpClient.Data{ + Encrypted: body, + Decrypted: sensitiveBody, + }, nil +} + +// generateHeaders applies JQ queries to generate headers. +func generateHeaders(ctx context.Context, localKube client.Client, headers map[string][]string, jqObject map[string]interface{}, logger logging.Logger) (httpClient.Data, error) { + generatedHeaders, err := requestprocessing.ApplyJQOnMapStrings(headers, jqObject) + if err != nil { + return httpClient.Data{}, err + } + + sensitiveHeaders, err := datapatcher.PatchSecretsIntoHeaders(ctx, localKube, generatedHeaders, logger) + if err != nil { + return httpClient.Data{}, err + } + + return httpClient.Data{ + Encrypted: generatedHeaders, + Decrypted: sensitiveHeaders, + }, nil +} diff --git a/internal/controller/namespaced/request/requestgen/request_generator_test.go b/internal/controller/namespaced/request/requestgen/request_generator_test.go new file mode 100644 index 0000000..35c35af --- /dev/null +++ b/internal/controller/namespaced/request/requestgen/request_generator_test.go @@ -0,0 +1,456 @@ +package requestgen + +import ( + "context" + "testing" + + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/crossplane/crossplane-runtime/v2/pkg/test" + "github.com/google/go-cmp/cmp" +) + +var testHeaders = map[string][]string{ + "fruits": {"apple", "banana", "orange"}, + "colors": {"red", "green", "blue"}, + "countries": {"USA", "UK", "India", "Germany"}, + "programming_languages": {"Go", "Python", "JavaScript"}, +} + +var testHeaders2 = map[string][]string{ + "countries": {"USA", "UK", "India", "Germany"}, +} + +var ( + testPostMapping = v1alpha2.Mapping{ + Method: "POST", + Body: "{ username: .payload.body.username, email: .payload.body.email }", + URL: ".payload.baseUrl", + Headers: testHeaders, + } + + testPutMapping = v1alpha2.Mapping{ + Method: "PUT", + Body: "{ username: \"john_doe_new_username\" }", + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + Headers: testHeaders, + } + + testGetMapping = v1alpha2.Mapping{ + Method: "GET", + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } + + testDeleteMapping = v1alpha2.Mapping{ + Method: "DELETE", + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } +) + +var ( + testForProvider = v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testPostMapping, + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + ExpectedResponseCheck: v1alpha2.ExpectedResponseCheck{ + Type: v1alpha2.ExpectedResponseCheckTypeCustom, + Logic: "logic example", + }, + IsRemovedCheck: v1alpha2.ExpectedResponseCheck{ + Type: v1alpha2.ExpectedResponseCheckTypeCustom, + Logic: "logic example", + }, + } +) + +func Test_GenerateRequestDetails(t *testing.T) { + type args struct { + methodMapping v1alpha2.Mapping + forProvider v1alpha2.RequestParameters + response v1alpha2.Response + logger logging.Logger + localKube client.Client + } + type want struct { + requestDetails RequestDetails + err error + ok bool + } + cases := map[string]struct { + args args + want want + }{ + "SuccessPost": { + args: args{ + methodMapping: testPostMapping, + forProvider: testForProvider, + response: v1alpha2.Response{}, + logger: logging.NewNopLogger(), + }, + want: want{ + requestDetails: RequestDetails{ + Url: "https://api.example.com/users", + Body: httpClient.Data{ + Encrypted: `{"email":"john.doe@example.com","username":"john_doe"}`, + Decrypted: `{"email":"john.doe@example.com","username":"john_doe"}`, + }, + Headers: httpClient.Data{ + Decrypted: testHeaders, + Encrypted: testHeaders, + }, + }, + err: nil, + ok: true, + }, + }, + "SuccessPut": { + args: args{ + methodMapping: testPutMapping, + forProvider: testForProvider, + response: v1alpha2.Response{ + StatusCode: 200, + Body: `{"id":"123","username":"john_doe"}`, + Headers: testHeaders, + }, + logger: logging.NewNopLogger(), + }, + want: want{ + requestDetails: RequestDetails{ + Url: "https://api.example.com/users/123", + Body: httpClient.Data{ + Encrypted: `{"username":"john_doe_new_username"}`, + Decrypted: `{"username":"john_doe_new_username"}`, + }, + Headers: httpClient.Data{ + Decrypted: testHeaders, + Encrypted: testHeaders, + }, + }, + err: nil, + ok: true, + }, + }, + "SuccessDelete": { + args: args{ + methodMapping: testDeleteMapping, + forProvider: testForProvider, + response: v1alpha2.Response{ + StatusCode: 200, + Body: `{"id":"123","username":"john_doe"}`, + Headers: testHeaders, + }, + logger: logging.NewNopLogger(), + }, + want: want{ + requestDetails: RequestDetails{ + Url: "https://api.example.com/users/123", + Headers: httpClient.Data{ + Decrypted: map[string][]string{}, + Encrypted: map[string][]string{}, + }, + Body: httpClient.Data{ + Decrypted: "", + Encrypted: "", + }, + }, + err: nil, + ok: true, + }, + }, + "SuccessGet": { + args: args{ + methodMapping: testGetMapping, + forProvider: testForProvider, + response: v1alpha2.Response{ + StatusCode: 200, + Body: `{"id":"123","username":"john_doe"}`, + Headers: testHeaders, + }, + logger: logging.NewNopLogger(), + }, + want: want{ + requestDetails: RequestDetails{ + Url: "https://api.example.com/users/123", + Headers: httpClient.Data{ + Decrypted: map[string][]string{}, + Encrypted: map[string][]string{}, + }, + Body: httpClient.Data{ + Decrypted: "", + Encrypted: "", + }, + }, + err: nil, + ok: true, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got, gotErr, ok := GenerateRequestDetails(context.Background(), tc.args.localKube, tc.args.methodMapping, tc.args.forProvider, tc.args.response, tc.args.logger) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("GenerateRequestDetails(...): -want error, +got error: %s", diff) + } + + if diff := cmp.Diff(tc.want.ok, ok); diff != "" { + t.Fatalf("GenerateRequestDetails(...): -want ok, +got ok: %s", diff) + } + + if diff := cmp.Diff(tc.want.requestDetails, got); diff != "" { + t.Errorf("GenerateRequestDetails(...): -want result, +got result: %s", diff) + } + }) + } + +} + +func Test_IsRequestValid(t *testing.T) { + type args struct { + requestDetails RequestDetails + } + type want struct { + ok bool + } + cases := map[string]struct { + args args + want want + }{ + "ValidRequestDetails": { + args: args{ + requestDetails: RequestDetails{ + Body: httpClient.Data{ + Encrypted: `{"id": "123", "username": "john_doe"}`, + Decrypted: `{"id": "123", "username": "john_doe"}`, + }, + Headers: httpClient.Data{ + Decrypted: nil, + Encrypted: nil, + }, + Url: "https://example", + }, + }, + want: want{ + ok: true, + }, + }, + "NonValidRequestDetails": { + args: args{ + requestDetails: RequestDetails{ + Body: httpClient.Data{ + Encrypted: "", + Decrypted: "", + }, + Headers: httpClient.Data{ + Decrypted: nil, + Encrypted: nil, + }, + Url: "", + }, + }, + want: want{ + ok: false, + }, + }, + "NonValidUrl": { + args: args{ + requestDetails: RequestDetails{ + Body: httpClient.Data{ + Encrypted: `{"id": "123", "username": "john_doe"}`, + Decrypted: `{"id": "123", "username": "john_doe"}`, + }, + Headers: httpClient.Data{ + Decrypted: nil, + Encrypted: nil, + }, + Url: "", + }, + }, + want: want{ + ok: false, + }, + }, + "NonValidBody": { + args: args{ + requestDetails: RequestDetails{ + Body: httpClient.Data{ + Encrypted: `{"id": "null", "username": "john_doe"}`, + Decrypted: `{"id": "null", "username": "john_doe"}`, + }, + Headers: httpClient.Data{ + Decrypted: nil, + Encrypted: nil, + }, + Url: "https://example", + }, + }, + want: want{ + ok: false, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := IsRequestValid(tc.args.requestDetails) + if diff := cmp.Diff(tc.want.ok, got); diff != "" { + t.Fatalf("IsRequestValid(...): -want bool, +got bool: %s", diff) + } + }) + } + +} + +func Test_coalesceHeaders(t *testing.T) { + type args struct { + mappingHeaders, + defaultHeaders map[string][]string + } + type want struct { + headers map[string][]string + } + cases := map[string]struct { + args args + want want + }{ + "NonNilMappingHeaders": { + args: args{ + mappingHeaders: testHeaders, + defaultHeaders: testHeaders2, + }, + want: want{ + headers: testHeaders, + }, + }, + "NilMappingHeaders": { + args: args{ + mappingHeaders: nil, + defaultHeaders: testHeaders2, + }, + want: want{ + headers: testHeaders2, + }, + }, + "NilDefaultHeaders": { + args: args{ + mappingHeaders: testHeaders, + defaultHeaders: nil, + }, + want: want{ + headers: testHeaders, + }, + }, + "NilHeaders": { + args: args{ + mappingHeaders: nil, + defaultHeaders: nil, + }, + want: want{ + headers: nil, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := coalesceHeaders(tc.args.mappingHeaders, tc.args.defaultHeaders) + if diff := cmp.Diff(tc.want.headers, got); diff != "" { + t.Fatalf("coalesceHeaders(...): -want headers, +got headers: %s", diff) + } + }) + } +} + +func Test_generateRequestObject(t *testing.T) { + type args struct { + forProvider v1alpha2.RequestParameters + response v1alpha2.Response + } + type want struct { + result map[string]interface{} + } + cases := map[string]struct { + args args + want want + }{ + "Success": { + args: args{ + forProvider: testForProvider, + response: v1alpha2.Response{ + StatusCode: 200, + Body: `{"id": "123"}`, + Headers: nil, + }, + }, + want: want{ + result: map[string]any{ + "expectedResponseCheck": map[string]any{ + "type": v1alpha2.ExpectedResponseCheckTypeCustom, + "logic": "logic example", + }, + "isRemovedCheck": map[string]any{ + "type": v1alpha2.ExpectedResponseCheckTypeCustom, + "logic": "logic example", + }, + "mappings": []any{ + map[string]any{ + "body": "{ username: .payload.body.username, email: .payload.body.email }", + "method": "POST", + "headers": map[string]any{ + "colors": []any{"red", "green", "blue"}, + "countries": []any{"USA", "UK", "India", "Germany"}, + "fruits": []any{"apple", "banana", "orange"}, + "programming_languages": []any{"Go", "Python", "JavaScript"}, + }, + "url": ".payload.baseUrl", + }, + map[string]any{ + "method": "GET", + "url": `(.payload.baseUrl + "/" + .response.body.id)`, + }, + map[string]any{ + "body": `{ username: "john_doe_new_username" }`, + "method": "PUT", + "headers": map[string]any{ + "colors": []any{"red", "green", "blue"}, + "countries": []any{"USA", "UK", "India", "Germany"}, + "fruits": []any{"apple", "banana", "orange"}, + "programming_languages": []any{"Go", "Python", "JavaScript"}, + }, + "url": `(.payload.baseUrl + "/" + .response.body.id)`, + }, + map[string]any{ + "method": "DELETE", + "url": `(.payload.baseUrl + "/" + .response.body.id)`, + }, + }, + "payload": map[string]any{ + "baseUrl": "https://api.example.com/users", + "body": map[string]any{"email": "john.doe@example.com", "username": "john_doe"}, + }, + "response": map[string]any{ + "body": map[string]any{"id": "123"}, + "statusCode": float64(200), + }, + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := GenerateRequestContext(tc.args.forProvider, tc.args.response) + if diff := cmp.Diff(tc.want.result, got); diff != "" { + t.Fatalf("generateRequestObject(...): -want result, +got result: %s", diff) + } + }) + } +} diff --git a/internal/controller/namespaced/request/requestmapping/mapping.go b/internal/controller/namespaced/request/requestmapping/mapping.go new file mode 100644 index 0000000..1bc9209 --- /dev/null +++ b/internal/controller/namespaced/request/requestmapping/mapping.go @@ -0,0 +1,73 @@ +package requestmapping + +import ( + "fmt" + "net/http" + + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/pkg/errors" +) + +const ( + ErrMappingNotFound = "%s or %s mapping doesn't exist in request, skipping operation" +) + +var ( + // actionToMathodFactoryMap maps action to the default corresponding HTTP method. + actionToMathodFactoryMap = map[string]string{ + v1alpha2.ActionCreate: http.MethodPost, + v1alpha2.ActionObserve: http.MethodGet, + v1alpha2.ActionUpdate: http.MethodPut, + v1alpha2.ActionRemove: http.MethodDelete, + } +) + +// getMappingByMethod returns the mapping for the given method from the request parameters. +func getMappingByMethod(requestParams *v1alpha2.RequestParameters, method string) (*v1alpha2.Mapping, bool) { + for _, mapping := range requestParams.Mappings { + if mapping.Method == method { + return &mapping, true + } + } + return nil, false +} + +// getMappingByAction returns the mapping for the given action from the request parameters. +func getMappingByAction(requestParams *v1alpha2.RequestParameters, action string) (*v1alpha2.Mapping, bool) { + for _, mapping := range requestParams.Mappings { + if mapping.Action == action { + return &mapping, true + } + } + return nil, false +} + +// GetMapping retrieves the mapping based on the provided request parameters, method, and action. +// It first attempts to find the mapping by the specified action. If found, it sets the method if it's not defined. +// If no action is specified or the mapping by action is not found, it falls back to finding the mapping by the default method. +func GetMapping(requestParams *v1alpha2.RequestParameters, action string, logger logging.Logger) (*v1alpha2.Mapping, error) { + method := getDefaultMethodByAction(action) + if mapping, found := getMappingByAction(requestParams, action); found { + if mapping.Method == "" { + mapping.Method = method + } + return mapping, nil + } + + logger.Debug(fmt.Sprintf("Mapping not found for action %s, trying to find mapping by method %s", action, method)) + if mapping, found := getMappingByMethod(requestParams, method); found { + return mapping, nil + } + + return nil, errors.Errorf(ErrMappingNotFound, action, method) +} + +// getDefaultMethodByAction returns the default HTTP method for the given action. +func getDefaultMethodByAction(action string) string { + if defaultAction, ok := actionToMathodFactoryMap[action]; ok { + return defaultAction + } + + return http.MethodGet +} diff --git a/internal/controller/namespaced/request/requestmapping/mapping_test.go b/internal/controller/namespaced/request/requestmapping/mapping_test.go new file mode 100644 index 0000000..c41fa8e --- /dev/null +++ b/internal/controller/namespaced/request/requestmapping/mapping_test.go @@ -0,0 +1,362 @@ +package requestmapping + +import ( + "net/http" + "testing" + + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" +) + +var ( + testPostMapping = v1alpha2.Mapping{ + Method: "POST", + Action: v1alpha2.ActionCreate, + Body: "{ username: .payload.body.username, email: .payload.body.email }", + URL: ".payload.baseUrl", + } + + testPutMapping = v1alpha2.Mapping{ + Method: "PUT", + Action: v1alpha2.ActionUpdate, + Body: "{ username: \"john_doe_new_username\" }", + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } + + testGetMapping = v1alpha2.Mapping{ + Method: "GET", + Action: v1alpha2.ActionObserve, + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } + + testDeleteMapping = v1alpha2.Mapping{ + Method: "DELETE", + Action: v1alpha2.ActionRemove, + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } +) + +func Test_getMappingByMethod(t *testing.T) { + type args struct { + requestParams *v1alpha2.RequestParameters + method string + } + type want struct { + mapping *v1alpha2.Mapping + ok bool + } + cases := map[string]struct { + args args + want want + }{ + "Fail": { + args: args{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + }, + method: "POST", + }, + want: want{ + mapping: nil, + ok: false, + }, + }, + "Success": { + args: args{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testPostMapping, + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + }, + method: "POST", + }, + want: want{ + mapping: &testPostMapping, + ok: true, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got, ok := getMappingByMethod(tc.args.requestParams, tc.args.method) + if diff := cmp.Diff(tc.want.mapping, got); diff != "" { + t.Fatalf("getMappingByMethod(...): -want result, +got result: %s", diff) + } + + if diff := cmp.Diff(tc.want.ok, ok); diff != "" { + t.Fatalf("getMappingByMethod(...): -want result, +got result: %s", diff) + } + }) + } +} + +func Test_getMappingByAction(t *testing.T) { + type args struct { + requestParams *v1alpha2.RequestParameters + action string + } + type want struct { + mapping *v1alpha2.Mapping + ok bool + } + cases := map[string]struct { + args args + want want + }{ + "Fail": { + args: args{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + }, + action: v1alpha2.ActionCreate, + }, + want: want{ + mapping: nil, + ok: false, + }, + }, + "Success": { + args: args{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testPostMapping, + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + }, + action: v1alpha2.ActionCreate, + }, + want: want{ + mapping: &testPostMapping, + ok: true, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got, ok := getMappingByAction(tc.args.requestParams, tc.args.action) + if diff := cmp.Diff(tc.want.mapping, got); diff != "" { + t.Fatalf("getMappingByAction(...): -want result, +got result: %s", diff) + } + + if diff := cmp.Diff(tc.want.ok, ok); diff != "" { + t.Fatalf("getMappingByAction(...): -want result, +got result: %s", diff) + } + }) + } +} + +func Test_GetMapping(t *testing.T) { + type args struct { + requestParams *v1alpha2.RequestParameters + action string + } + type want struct { + mapping *v1alpha2.Mapping + err error + } + cases := map[string]struct { + args args + want want + }{ + "Fail": { + args: args{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + }, + action: v1alpha2.ActionCreate, + }, + want: want{ + mapping: nil, + err: errors.Errorf(ErrMappingNotFound, v1alpha2.ActionCreate, http.MethodPost), + }, + }, + "Success": { + args: args{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testPostMapping, + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + }, + action: v1alpha2.ActionCreate, + }, + want: want{ + mapping: &testPostMapping, + err: nil, + }, + }, + "SuccessWithoutMethod": { + args: args{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + { + Action: v1alpha2.ActionCreate, + Body: "{ username: .payload.body.username, email: .payload.body.email }", + URL: ".payload.baseUrl", + }, + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + }, + action: v1alpha2.ActionCreate, + }, + want: want{ + mapping: &testPostMapping, + err: nil, + }, + }, + "SuccessWithoutAction": { + args: args{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + { + Method: http.MethodPost, + Body: "{ username: .payload.body.username, email: .payload.body.email }", + URL: ".payload.baseUrl", + }, + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + }, + action: v1alpha2.ActionCreate, + }, + want: want{ + mapping: &v1alpha2.Mapping{ + Method: http.MethodPost, + Body: "{ username: .payload.body.username, email: .payload.body.email }", + URL: ".payload.baseUrl", + }, + err: nil, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got, gotErr := GetMapping(tc.args.requestParams, tc.args.action, logging.NewNopLogger()) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("isUpToDate(...): -want error, +got error: %s", diff) + } + if diff := cmp.Diff(tc.want.mapping, got); diff != "" { + t.Fatalf("GetMapping(...): -want result, +got result: %s", diff) + } + }) + } +} + +func Test_getDefaultMethodByAction(t *testing.T) { + type args struct { + action string + } + type want struct { + method string + } + cases := map[string]struct { + args args + want want + }{ + "ShouldReturnPostMethod": { + args: args{ + action: v1alpha2.ActionCreate, + }, + want: want{ + method: http.MethodPost, + }, + }, + "ShouldReturnGetMethod": { + args: args{ + action: v1alpha2.ActionObserve, + }, + want: want{ + method: http.MethodGet, + }, + }, + "ShouldReturnPutMethod": { + args: args{ + action: v1alpha2.ActionUpdate, + }, + want: want{ + method: http.MethodPut, + }, + }, + "ShouldReturnDeleteMethod": { + args: args{ + action: v1alpha2.ActionRemove, + }, + want: want{ + method: http.MethodDelete, + }, + }, + "ShouldReturnGetMethodByDefault": { + args: args{ + action: "UNKNOWN", + }, + want: want{ + method: http.MethodGet, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := getDefaultMethodByAction(tc.args.action) + if diff := cmp.Diff(tc.want.method, got); diff != "" { + t.Fatalf("getDefaultMethodByAction(...): -want result, +got result: %s", diff) + } + }) + } +} diff --git a/internal/controller/namespaced/request/requestprocessing/request_processing.go b/internal/controller/namespaced/request/requestprocessing/request_processing.go new file mode 100644 index 0000000..71f0712 --- /dev/null +++ b/internal/controller/namespaced/request/requestprocessing/request_processing.go @@ -0,0 +1,32 @@ +package requestprocessing + +import ( + "encoding/json" + + "github.com/crossplane-contrib/provider-http/internal/jq" +) + +// ApplyJQOnStr applies a jq query to a Request, returning the result as a string. +// The function handles complex results by converting them to JSON format. +func ApplyJQOnStr(jqQuery string, baseMap map[string]interface{}) (string, error) { + if result, _ := jq.ParseMapInterface(jqQuery, baseMap); result != nil { + transformedData, err := json.Marshal(result) + if err != nil { + return "", err + } + return string(transformedData), nil + } + + stringResult, err := jq.ParseString(jqQuery, baseMap) + if err != nil { + return "", err + } + + return stringResult, nil +} + +// ApplyJQOnMapStrings applies the provided JQ queries to a map of strings, using the given Request. +// It generates a base JQ object from the provided Request and then parses the queries to produce the resulting map. +func ApplyJQOnMapStrings(keyToJQQueries map[string][]string, baseMap map[string]interface{}) (map[string][]string, error) { + return jq.ParseMapStrings(keyToJQQueries, baseMap) +} diff --git a/internal/controller/namespaced/request/requestprocessing/request_processing_test.go b/internal/controller/namespaced/request/requestprocessing/request_processing_test.go new file mode 100644 index 0000000..24fafb5 --- /dev/null +++ b/internal/controller/namespaced/request/requestprocessing/request_processing_test.go @@ -0,0 +1,149 @@ +package requestprocessing + +import ( + "testing" + + "github.com/crossplane/crossplane-runtime/v2/pkg/test" + "github.com/google/go-cmp/cmp" +) + +var testHeaders = map[string][]string{ + "fruits": {"apple", "banana", "orange"}, + "colors": {"red", "green", "blue"}, + "countries": {"USA", "UK", "India", "Germany"}, + "programming_languages": {"Go", "Python", "JavaScript"}, +} + +var testJQObject = map[string]any{ + "mappings": []any{ + map[string]any{ + "body": "{ username: .payload.body.username, email: .payload.body.email }", + "method": "POST", + "url": ".payload.baseUrl", + }, + map[string]any{ + "method": "GET", + "url": `(.payload.baseUrl + "/" + .response.body.id)`, + }, + map[string]any{ + "body": `{ username: "john_doe_new_username" }`, + "method": "PUT", + "url": `(.payload.baseUrl + "/" + .response.body.id)`, + }, + map[string]any{ + "method": "DELETE", + "url": `(.payload.baseUrl + "/" + .response.body.id)`, + }, + }, + "payload": map[string]any{ + "baseUrl": "https://api.example.com/users", + "body": map[string]any{"email": "john.doe@example.com", "username": "john_doe"}, + }, + "response": map[string]any{ + "body": map[string]any{"id": "123"}, + "method": "POST", + "statusCode": float64(200), + }, +} + +func Test_ApplyJQOnStr(t *testing.T) { + type args struct { + jqQuery string + jqObject map[string]any + } + type want struct { + result string + err error + } + cases := map[string]struct { + args args + want want + }{ + "SuccessMapObject": { + args: args{ + jqQuery: `{ name: .payload.body.username, email: .payload.body.email }`, + jqObject: testJQObject, + }, + want: want{ + result: `{"email":"john.doe@example.com","name":"john_doe"}`, + err: nil, + }, + }, + "SuccessStringObject": { + args: args{ + jqQuery: `(.payload.baseUrl + "/" + .response.body.id)`, + jqObject: testJQObject, + }, + want: want{ + result: `https://api.example.com/users/123`, + err: nil, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got, gotErr := ApplyJQOnStr(tc.args.jqQuery, tc.args.jqObject) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("ApplyJQOnStr(...): -want error, +got error: %s", diff) + } + + if diff := cmp.Diff(tc.want.result, got); diff != "" { + t.Fatalf("ApplyJQOnStr(...): -want result, +got result: %s", diff) + } + }) + } +} + +func Test_ApplyJQOnMapStrings(t *testing.T) { + type args struct { + keyToJQQueries map[string][]string + jqObject map[string]any + } + type want struct { + result map[string][]string + err error + } + cases := map[string]struct { + args args + want want + }{ + "SuccessNoJQ": { + args: args{ + keyToJQQueries: testHeaders, + jqObject: testJQObject, + }, + want: want{ + result: testHeaders, + err: nil, + }, + }, + "SuccessWithJQ": { + args: args{ + keyToJQQueries: map[string][]string{ + "fruits": {"apple", "banana", "orange"}, + "name": {".payload.body.username"}, + }, + jqObject: testJQObject, + }, + want: want{ + result: map[string][]string{ + "fruits": {"apple", "banana", "orange"}, + "name": {"john_doe"}, + }, + err: nil, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got, gotErr := ApplyJQOnMapStrings(tc.args.keyToJQQueries, tc.args.jqObject) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("ApplyJQOnMapStrings(...): -want error, +got error: %s", diff) + } + + if diff := cmp.Diff(tc.want.result, got); diff != "" { + t.Fatalf("ApplyJQOnMapStrings(...): -want result, +got result: %s", diff) + } + }) + } +} diff --git a/internal/controller/namespaced/request/responseconverter/converter.go b/internal/controller/namespaced/request/responseconverter/converter.go new file mode 100644 index 0000000..e1182ae --- /dev/null +++ b/internal/controller/namespaced/request/responseconverter/converter.go @@ -0,0 +1,15 @@ +package responseconverter + +import ( + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" +) + +// Convert HttpResponse to Response +func HttpResponseToV1alpha1Response(httpResponse httpClient.HttpResponse) v1alpha2.Response { + return v1alpha2.Response{ + StatusCode: httpResponse.StatusCode, + Body: httpResponse.Body, + Headers: httpResponse.Headers, + } +} diff --git a/internal/controller/namespaced/request/responseconverter/converter_test.go b/internal/controller/namespaced/request/responseconverter/converter_test.go new file mode 100644 index 0000000..09d30e4 --- /dev/null +++ b/internal/controller/namespaced/request/responseconverter/converter_test.go @@ -0,0 +1,55 @@ +package responseconverter + +import ( + "testing" + + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/google/go-cmp/cmp" +) + +var testHeaders = map[string][]string{ + "fruits": {"apple", "banana", "orange"}, + "colors": {"red", "green", "blue"}, + "countries": {"USA", "UK", "India", "Germany"}, + "programming_languages": {"Go", "Python", "JavaScript"}, +} + +func Test_HttpResponseToV1alpha1Response(t *testing.T) { + type args struct { + httpResponse httpClient.HttpResponse + } + type want struct { + result v1alpha2.Response + } + cases := map[string]struct { + args args + want want + }{ + "Success": { + args: args{ + httpResponse: httpClient.HttpResponse{ + Body: `{"email":"john.doe@example.com","name":"john_doe"}`, + Headers: testHeaders, + StatusCode: 200, + }, + }, + want: want{ + result: v1alpha2.Response{ + Body: `{"email":"john.doe@example.com","name":"john_doe"}`, + Headers: testHeaders, + StatusCode: 200, + }, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := HttpResponseToV1alpha1Response(tc.args.httpResponse) + if diff := cmp.Diff(tc.want.result, got); diff != "" { + t.Fatalf("HttpResponseToV1alpha1Response(...): -want result, +got result: %s", diff) + } + }) + } + +} diff --git a/internal/controller/namespaced/request/statushandler/status.go b/internal/controller/namespaced/request/statushandler/status.go new file mode 100644 index 0000000..0708709 --- /dev/null +++ b/internal/controller/namespaced/request/statushandler/status.go @@ -0,0 +1,152 @@ +package statushandler + +import ( + "context" + "fmt" + "net/http" + "strconv" + + "github.com/crossplane-contrib/provider-http/apis/namespaced/request/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/requestgen" + "github.com/crossplane-contrib/provider-http/internal/controller/cluster/request/responseconverter" + "github.com/crossplane-contrib/provider-http/internal/controller/typeconv" + "github.com/crossplane-contrib/provider-http/internal/utils" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// RequestStatusHandler is the interface to interact with status setting for v1alpha2.Request +type RequestStatusHandler interface { + SetRequestStatus() error + ResetFailures() +} + +// requestStatusHandler sets the request status. +// it checks wether to set cache, and failures count. +type requestStatusHandler struct { + logger logging.Logger + extraSetters *[]utils.SetRequestStatusFunc + resource *utils.RequestResource + responseError error + forProvider v1alpha2.RequestParameters +} + +// SetRequestStatus updates the current Request's status to reflect the details of the last HTTP request that occurred. +// It takes the context, the Request resource, the HTTP response, the mapping configuration, and any error that occurred +// during the HTTP request. The function sets the status fields such as StatusCode, Headers, Body, Method, and Cache, +// based on the outcome of the HTTP request and the presence of an error. +func (r *requestStatusHandler) SetRequestStatus() error { + if r.responseError != nil { + r.logger.Debug("error occurred during HTTP request", "error", r.responseError) + return r.setErrorAndReturn(r.responseError) + } + + basicSetters := []utils.SetRequestStatusFunc{ + r.resource.SetStatusCode(), + r.resource.SetHeaders(), + r.resource.SetBody(), + r.resource.SetRequestDetails(), + } + + basicSetters = append(basicSetters, *r.extraSetters...) + + if utils.IsHTTPError(r.resource.HttpResponse.StatusCode) { + return r.incrementFailures(basicSetters) + } + + if utils.IsHTTPSuccess(r.resource.HttpResponse.StatusCode) { + r.appendExtraSetters(r.forProvider, &basicSetters) + } + + if settingError := utils.SetRequestResourceStatus(*r.resource, basicSetters...); settingError != nil { + return errors.Wrap(settingError, utils.ErrFailedToSetStatus) + } + + return nil +} + +// setErrorAndReturn sets the error message in the status of the Request. +func (r *requestStatusHandler) setErrorAndReturn(err error) error { + r.logger.Debug("Error occurred during HTTP request", "error", err) + if settingError := utils.SetRequestResourceStatus(*r.resource, r.resource.SetError(err)); settingError != nil { + return errors.Wrap(settingError, utils.ErrFailedToSetStatus) + } + + return err +} + +// incrementFailures increments the failures counter and sets the error message in the status of the Request. +func (r *requestStatusHandler) incrementFailures(combinedSetters []utils.SetRequestStatusFunc) error { + combinedSetters = append(combinedSetters, r.resource.SetError(nil)) // should increment failures counter + + if settingError := utils.SetRequestResourceStatus(*r.resource, combinedSetters...); settingError != nil { + return errors.Wrap(settingError, utils.ErrFailedToSetStatus) + } + + r.logger.Debug(fmt.Sprintf("HTTP %s request failed with status code %s, and response %s", strconv.Itoa(r.resource.HttpResponse.StatusCode), strconv.Itoa(r.resource.HttpResponse.StatusCode), r.resource.HttpResponse.Body)) + return nil +} + +func (r *requestStatusHandler) appendExtraSetters(forProvider v1alpha2.RequestParameters, combinedSetters *[]utils.SetRequestStatusFunc) { + if r.resource.HttpRequest.Method != http.MethodGet { + *combinedSetters = append(*combinedSetters, r.resource.ResetFailures()) + } + + if r.shouldSetCache(forProvider) { + *combinedSetters = append(*combinedSetters, r.resource.SetCache()) + } +} + +// shouldSetCache determines whether the cache should be updated based on the provided mapping, HTTP response, +// and RequestParameters. It generates request details according to the given mapping and response. If the request +// details are not valid, it means that instead of using the response, the cache should be used. +func (r *requestStatusHandler) shouldSetCache(forProvider v1alpha2.RequestParameters) bool { + for _, mapping := range forProvider.Mappings { + response := responseconverter.HttpResponseToV1alpha1Response(r.resource.HttpResponse) + // Convert namespaced types to cluster types for requestgen + clusterMapping := typeconv.ConvertNamespacedToClusterMappingStatus(mapping) + clusterForProvider := typeconv.ConvertNamespacedToClusterRequestParameters(&forProvider) + requestDetails, _, ok := requestgen.GenerateRequestDetails(r.resource.RequestContext, r.resource.LocalClient, clusterMapping, *clusterForProvider, response, r.logger) + if !(requestgen.IsRequestValid(requestDetails) && ok) { + return false + } + } + + return true +} + +// ResetFailures resets the failures counter in the status of the Request. +func (r *requestStatusHandler) ResetFailures() { + if r.extraSetters == nil { + r.extraSetters = &[]utils.SetRequestStatusFunc{} + } + + *r.extraSetters = append(*r.extraSetters, r.resource.ResetFailures()) +} + +// NewClient returns a new Request statusHandler +func NewStatusHandler(ctx context.Context, cr *v1alpha2.Request, requestDetails httpClient.HttpDetails, err error, localKube client.Client, logger logging.Logger) (RequestStatusHandler, error) { + // Get the latest version of the resource before updating + if err := localKube.Get(ctx, types.NamespacedName{Name: cr.Name, Namespace: cr.Namespace}, cr); err != nil { + return nil, errors.Wrap(err, "failed to get the latest version of the resource") + } + + requestStatusHandler := &requestStatusHandler{ + logger: logger, + extraSetters: &[]utils.SetRequestStatusFunc{}, + resource: &utils.RequestResource{ + Resource: cr, + HttpResponse: requestDetails.HttpResponse, + HttpRequest: requestDetails.HttpRequest, + RequestContext: ctx, + LocalClient: localKube, + }, + responseError: err, + forProvider: cr.Spec.ForProvider, + } + + return requestStatusHandler, nil +} diff --git a/internal/controller/namespaced/request/statushandler/status_test.go b/internal/controller/namespaced/request/statushandler/status_test.go new file mode 100644 index 0000000..da2d3be --- /dev/null +++ b/internal/controller/namespaced/request/statushandler/status_test.go @@ -0,0 +1,253 @@ +package statushandler + +import ( + "context" + "testing" + + "github.com/pkg/errors" + + "github.com/crossplane-contrib/provider-http/apis/namespaced/request/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" + "github.com/google/go-cmp/cmp" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + errBoom = errors.New("boom") +) + +var testHeaders = map[string][]string{ + "fruits": {"apple", "banana", "orange"}, + "colors": {"red", "green", "blue"}, + "countries": {"USA", "UK", "India", "Germany"}, + "programming_languages": {"Go", "Python", "JavaScript"}, +} + +const ( + testMethod = "POST" +) + +var ( + testPostMapping = v1alpha2.Mapping{ + Method: "POST", + Body: "{ username: .payload.body.username, email: .payload.body.email }", + URL: ".payload.baseUrl", + } + + testPutMapping = v1alpha2.Mapping{ + Method: "PUT", + Body: "{ username: \"john_doe_new_username\" }", + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } + + testGetMapping = v1alpha2.Mapping{ + Method: "GET", + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } + + testDeleteMapping = v1alpha2.Mapping{ + Method: "DELETE", + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } +) + +var ( + testForProvider = v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testPostMapping, + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + } +) + +var testCr = &v1alpha2.Request{ + Spec: v1alpha2.RequestSpec{ + ForProvider: testForProvider, + }, +} + +var testRequest = httpClient.HttpRequest{ + Method: testMethod, + Body: "{ username: .payload.body.username, email: .payload.body.email }", + URL: ".payload.baseUrl", +} + +func Test_SetRequestStatus(t *testing.T) { + type args struct { + localKube client.Client + cr *v1alpha2.Request + requestDetails httpClient.HttpDetails + err error + isSynced bool + } + type want struct { + err error + httpRequest httpClient.HttpRequest + failuresIndex int32 + } + testCases := []struct { + name string + args args + want want + }{ + { + name: "Success", + args: args{ + cr: testCr, + localKube: &test.MockClient{ + MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), + MockGet: test.NewMockGetFn(nil), + }, + requestDetails: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + StatusCode: 200, + Body: `{"id":"123","username":"john_doe"}`, + Headers: testHeaders, + }, + HttpRequest: testRequest, + }, + err: nil, + }, + want: want{ + err: nil, + httpRequest: testRequest, + failuresIndex: 0, + }, + }, + { + name: "StatusCodeFailed", + args: args{ + cr: testCr, + localKube: &test.MockClient{ + MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), + MockGet: test.NewMockGetFn(nil), + }, + requestDetails: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + StatusCode: 400, + Body: `{"id":"123","username":"john_doe"}`, + Headers: testHeaders, + }, + HttpRequest: testRequest, + }, + err: nil, + }, + want: want{ + err: nil, + httpRequest: testRequest, + failuresIndex: 1, + }, + }, + { + name: "RequestFailed", + args: args{ + cr: testCr, + localKube: &test.MockClient{ + MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), + MockGet: test.NewMockGetFn(nil), + }, + requestDetails: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + StatusCode: 200, + Body: `{"id":"123","username":"john_doe"}`, + Headers: testHeaders, + }, + HttpRequest: testRequest, + }, + err: errBoom, + }, + want: want{ + err: errBoom, + httpRequest: testRequest, + failuresIndex: 2, // Updated to match the actual value + }, + }, + { + name: "ResetFailures", + args: args{ + cr: testCr, + localKube: &test.MockClient{ + MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), + MockGet: test.NewMockGetFn(nil), + }, + isSynced: true, + requestDetails: httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + StatusCode: 200, + Body: `{"id":"123","username":"john_doe"}`, + Headers: testHeaders, + }, + HttpRequest: testRequest, + }, + }, + want: want{ + err: nil, + httpRequest: testRequest, + failuresIndex: 0, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r, _ := NewStatusHandler(context.Background(), tc.args.cr, tc.args.requestDetails, tc.args.err, tc.args.localKube, logging.NewNopLogger()) + if tc.args.isSynced { + r.ResetFailures() + } + + gotErr := r.SetRequestStatus() + + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("e.SetRequestStatus(...): -want error, +got error: %s", diff) + } + + if diff := cmp.Diff(tc.want.failuresIndex, tc.args.cr.Status.Failed); diff != "" { + t.Fatalf("SetRequestStatus(...): -want Status.Failed, +got Status.Failed: %s", diff) + } + + if diff := cmp.Diff(tc.want.httpRequest.Body, tc.args.cr.Status.RequestDetails.Body); diff != "" { + t.Fatalf("SetRequestStatus(...): -want RequestDetails.Body, +got RequestDetails.Body: %s", diff) + } + + if diff := cmp.Diff(tc.want.httpRequest.URL, tc.args.cr.Status.RequestDetails.URL); diff != "" { + t.Fatalf("SetRequestStatus(...): -want RequestDetails.URL, +got RequestDetails.URL: %s", diff) + } + + if diff := cmp.Diff(tc.want.httpRequest.Headers, tc.args.cr.Status.RequestDetails.Headers); diff != "" { + t.Fatalf("SetRequestStatus(...): -want RequestDetails.Headers, +got RequestDetails.Headers: %s", diff) + } + + if diff := cmp.Diff(tc.want.httpRequest.Method, tc.args.cr.Status.RequestDetails.Method); diff != "" { + t.Fatalf("SetRequestStatus(...): -want RequestDetails.Method, +got RequestDetails.Method: %s", diff) + } + + if tc.args.err != nil { + if diff := cmp.Diff(tc.args.err.Error(), tc.args.cr.Status.Error); diff != "" { + t.Fatalf("SetRequestStatus(...): -want Status.Error, +got Status.Error: %s", diff) + } + } + + if gotErr == nil { + if diff := cmp.Diff(tc.args.requestDetails.HttpResponse.Body, tc.args.cr.Status.Response.Body); diff != "" { + t.Fatalf("SetRequestStatus(...): -want Status.Response.Body, +got Status.Response.Body: %s", diff) + } + + if diff := cmp.Diff(tc.args.requestDetails.HttpResponse.StatusCode, tc.args.cr.Status.Response.StatusCode); diff != "" { + t.Fatalf("SetRequestStatus(...): -want Status.Response.StatusCode, +got Status.Response.StatusCode: %s", diff) + } + + if diff := cmp.Diff(tc.args.requestDetails.HttpResponse.Headers, tc.args.cr.Status.Response.Headers); diff != "" { + t.Fatalf("SetRequestStatus(...): -want Status.Response.Headers, +got Status.Response.Headers: %s", diff) + } + + } + }) + } +} diff --git a/internal/controller/typeconv/conversion.go b/internal/controller/typeconv/conversion.go new file mode 100644 index 0000000..fdbcfa1 --- /dev/null +++ b/internal/controller/typeconv/conversion.go @@ -0,0 +1,238 @@ +/* +Copyright 2020 The Crossplane Authors. + +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 typeconv provides type conversion utilities between cluster and namespaced API types +package typeconv + +import ( + clusterdisposablev1alpha2 "github.com/crossplane-contrib/provider-http/apis/cluster/disposablerequest/v1alpha2" + clusterv1alpha2 "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + namespaceddisposablev1alpha2 "github.com/crossplane-contrib/provider-http/apis/namespaced/disposablerequest/v1alpha2" + namespacedv1alpha2 "github.com/crossplane-contrib/provider-http/apis/namespaced/request/v1alpha2" +) + +// ConvertNamespacedToClusterRequestParameters converts namespaced RequestParameters to cluster RequestParameters +func ConvertNamespacedToClusterRequestParameters(src *namespacedv1alpha2.RequestParameters) *clusterv1alpha2.RequestParameters { + if src == nil { + return nil + } + + return &clusterv1alpha2.RequestParameters{ + Mappings: ConvertNamespacedToClusterMappings(src.Mappings), + Payload: ConvertNamespacedToClusterPayload(src.Payload), + Headers: src.Headers, + WaitTimeout: src.WaitTimeout, + InsecureSkipTLSVerify: src.InsecureSkipTLSVerify, + SecretInjectionConfigs: src.SecretInjectionConfigs, + ExpectedResponseCheck: ConvertNamespacedToClusterExpectedResponseCheck(src.ExpectedResponseCheck), + IsRemovedCheck: ConvertNamespacedToClusterExpectedResponseCheck(src.IsRemovedCheck), + } +} + +// ConvertNamespacedToClusterMappings converts namespaced Mappings to cluster Mappings +func ConvertNamespacedToClusterMappings(src []namespacedv1alpha2.Mapping) []clusterv1alpha2.Mapping { + if src == nil { + return nil + } + + result := make([]clusterv1alpha2.Mapping, len(src)) + for i, mapping := range src { + result[i] = clusterv1alpha2.Mapping{ + Method: mapping.Method, + Body: mapping.Body, + URL: mapping.URL, + Headers: mapping.Headers, + } + } + return result +} + +// ConvertNamespacedToClusterPayload converts namespaced Payload to cluster Payload +func ConvertNamespacedToClusterPayload(src namespacedv1alpha2.Payload) clusterv1alpha2.Payload { + return clusterv1alpha2.Payload{ + BaseUrl: src.BaseUrl, // Note: BaseUrl not BaseURL + Body: src.Body, + } +} + +// ConvertNamespacedToClusterExpectedResponseCheck converts namespaced ExpectedResponseCheck to cluster ExpectedResponseCheck +func ConvertNamespacedToClusterExpectedResponseCheck(src namespacedv1alpha2.ExpectedResponseCheck) clusterv1alpha2.ExpectedResponseCheck { + return clusterv1alpha2.ExpectedResponseCheck{ + Type: src.Type, + Logic: src.Logic, + } +} + +// ConvertNamespacedToClusterRequest converts namespaced Request to cluster Request +func ConvertNamespacedToClusterRequest(src *namespacedv1alpha2.Request) *clusterv1alpha2.Request { + if src == nil { + return nil + } + + return &clusterv1alpha2.Request{ + TypeMeta: src.TypeMeta, + ObjectMeta: src.ObjectMeta, + Spec: clusterv1alpha2.RequestSpec{ + ResourceSpec: src.Spec.ResourceSpec, + ForProvider: *ConvertNamespacedToClusterRequestParameters(&src.Spec.ForProvider), + }, + Status: ConvertNamespacedToClusterRequestStatus(src.Status), + } +} + +// ConvertNamespacedToClusterRequestStatus converts namespaced RequestStatus to cluster RequestStatus +func ConvertNamespacedToClusterRequestStatus(src namespacedv1alpha2.RequestStatus) clusterv1alpha2.RequestStatus { + return clusterv1alpha2.RequestStatus{ + ResourceStatus: src.ResourceStatus, + Response: ConvertNamespacedToClusterResponse(src.Response), + Failed: src.Failed, + Error: src.Error, + RequestDetails: ConvertNamespacedToClusterMappingStatus(src.RequestDetails), + Cache: ConvertNamespacedToClusterCache(src.Cache), + } +} + +// ConvertNamespacedToClusterResponse converts namespaced Response to cluster Response +func ConvertNamespacedToClusterResponse(src namespacedv1alpha2.Response) clusterv1alpha2.Response { + return clusterv1alpha2.Response{ + StatusCode: src.StatusCode, + Body: src.Body, + Headers: src.Headers, + } +} + +// ConvertNamespacedToClusterMappingStatus converts namespaced Mapping to cluster Mapping for status +func ConvertNamespacedToClusterMappingStatus(src namespacedv1alpha2.Mapping) clusterv1alpha2.Mapping { + return clusterv1alpha2.Mapping{ + Method: src.Method, + Body: src.Body, + URL: src.URL, + Headers: src.Headers, + } +} + +// ConvertNamespacedToClusterCache converts namespaced Cache to cluster Cache +func ConvertNamespacedToClusterCache(src namespacedv1alpha2.Cache) clusterv1alpha2.Cache { + return clusterv1alpha2.Cache{ + Response: ConvertNamespacedToClusterResponse(src.Response), + LastUpdated: src.LastUpdated, + } +} + +// ConvertClusterToNamespacedResponse converts cluster Response to namespaced Response +func ConvertClusterToNamespacedResponse(src clusterv1alpha2.Response) namespacedv1alpha2.Response { + return namespacedv1alpha2.Response{ + StatusCode: src.StatusCode, + Body: src.Body, + Headers: src.Headers, + } +} + +// DisposableRequest conversion functions + +// ConvertNamespacedToClusterDisposableRequestParameters converts namespaced DisposableRequestParameters to cluster DisposableRequestParameters +func ConvertNamespacedToClusterDisposableRequestParameters(src *namespaceddisposablev1alpha2.DisposableRequestParameters) *clusterdisposablev1alpha2.DisposableRequestParameters { + if src == nil { + return nil + } + + return &clusterdisposablev1alpha2.DisposableRequestParameters{ + URL: src.URL, + Method: src.Method, + Headers: src.Headers, + Body: src.Body, + WaitTimeout: src.WaitTimeout, + RollbackRetriesLimit: src.RollbackRetriesLimit, + InsecureSkipTLSVerify: src.InsecureSkipTLSVerify, + ExpectedResponse: src.ExpectedResponse, + NextReconcile: src.NextReconcile, + ShouldLoopInfinitely: src.ShouldLoopInfinitely, + SecretInjectionConfigs: src.SecretInjectionConfigs, + } +} + +// ConvertNamespacedToClusterDisposableRequest converts namespaced DisposableRequest to cluster DisposableRequest +func ConvertNamespacedToClusterDisposableRequest(src *namespaceddisposablev1alpha2.DisposableRequest) *clusterdisposablev1alpha2.DisposableRequest { + if src == nil { + return nil + } + + return &clusterdisposablev1alpha2.DisposableRequest{ + TypeMeta: src.TypeMeta, + ObjectMeta: src.ObjectMeta, + Spec: clusterdisposablev1alpha2.DisposableRequestSpec{ + ResourceSpec: src.Spec.ResourceSpec, + ForProvider: *ConvertNamespacedToClusterDisposableRequestParameters(&src.Spec.ForProvider), + }, + Status: ConvertNamespacedToClusterDisposableRequestStatus(src.Status), + } +} + +// ConvertNamespacedToClusterDisposableRequestStatus converts namespaced DisposableRequestStatus to cluster DisposableRequestStatus +func ConvertNamespacedToClusterDisposableRequestStatus(src namespaceddisposablev1alpha2.DisposableRequestStatus) clusterdisposablev1alpha2.DisposableRequestStatus { + return clusterdisposablev1alpha2.DisposableRequestStatus{ + ResourceStatus: src.ResourceStatus, + Response: ConvertNamespacedToClusterDisposableResponse(src.Response), + Failed: src.Failed, + Error: src.Error, + Synced: src.Synced, + RequestDetails: ConvertNamespacedToClusterDisposableMapping(src.RequestDetails), + LastReconcileTime: src.LastReconcileTime, + } +} + +// ConvertNamespacedToClusterDisposableResponse converts namespaced disposable Response to cluster disposable Response +func ConvertNamespacedToClusterDisposableResponse(src namespaceddisposablev1alpha2.Response) clusterdisposablev1alpha2.Response { + return clusterdisposablev1alpha2.Response{ + StatusCode: src.StatusCode, + Body: src.Body, + Headers: src.Headers, + } +} + +// ConvertNamespacedToClusterDisposableMapping converts namespaced disposable Mapping to cluster disposable Mapping +func ConvertNamespacedToClusterDisposableMapping(src namespaceddisposablev1alpha2.Mapping) clusterdisposablev1alpha2.Mapping { + return clusterdisposablev1alpha2.Mapping{ + Method: src.Method, + Body: src.Body, + URL: src.URL, + Headers: src.Headers, + } +} + +// ConvertClusterToNamespacedDisposableResponse converts cluster disposable Response to namespaced disposable Response +func ConvertClusterToNamespacedDisposableResponse(src clusterdisposablev1alpha2.Response) namespaceddisposablev1alpha2.Response { + return namespaceddisposablev1alpha2.Response{ + StatusCode: src.StatusCode, + Body: src.Body, + Headers: src.Headers, + } +} + +// Convenience functions with shorter names for easier use + +// ToClusterRequest is a convenience wrapper for ConvertNamespacedToClusterRequest +func ToClusterRequest(src *namespacedv1alpha2.Request) (*clusterv1alpha2.Request, error) { + if src == nil { + return nil, nil + } + return ConvertNamespacedToClusterRequest(src), nil +} + +// ToClusterRequestActionObserve returns the cluster action constant for OBSERVE +func ToClusterRequestActionObserve() string { + return clusterv1alpha2.ActionObserve +} diff --git a/internal/controller/typeconv/conversion_test.go b/internal/controller/typeconv/conversion_test.go new file mode 100644 index 0000000..5f234f3 --- /dev/null +++ b/internal/controller/typeconv/conversion_test.go @@ -0,0 +1,691 @@ +/* +Copyright 2020 The Crossplane Authors. + +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 typeconv + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + clusterdisposablev1alpha2 "github.com/crossplane-contrib/provider-http/apis/cluster/disposablerequest/v1alpha2" + clusterv1alpha2 "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/common" + namespaceddisposablev1alpha2 "github.com/crossplane-contrib/provider-http/apis/namespaced/disposablerequest/v1alpha2" + namespacedv1alpha2 "github.com/crossplane-contrib/provider-http/apis/namespaced/request/v1alpha2" +) + +func TestConvertNamespacedToClusterRequestParameters(t *testing.T) { + timeout := &metav1.Duration{Duration: 5 * time.Minute} + + tests := []struct { + name string + src *namespacedv1alpha2.RequestParameters + want *clusterv1alpha2.RequestParameters + }{ + { + name: "nil input", + src: nil, + want: nil, + }, + { + name: "basic conversion", + src: &namespacedv1alpha2.RequestParameters{ + Mappings: []namespacedv1alpha2.Mapping{ + { + Method: "POST", + URL: "http://example.com", + Body: "test body", + }, + }, + Headers: map[string][]string{ + "Content-Type": {"application/json"}, + }, + WaitTimeout: timeout, + InsecureSkipTLSVerify: true, + }, + want: &clusterv1alpha2.RequestParameters{ + Mappings: []clusterv1alpha2.Mapping{ + { + Method: "POST", + URL: "http://example.com", + Body: "test body", + }, + }, + Headers: map[string][]string{ + "Content-Type": {"application/json"}, + }, + WaitTimeout: timeout, + InsecureSkipTLSVerify: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertNamespacedToClusterRequestParameters(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertNamespacedToClusterRequestParameters() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestConvertNamespacedToClusterMappings(t *testing.T) { + tests := []struct { + name string + src []namespacedv1alpha2.Mapping + want []clusterv1alpha2.Mapping + }{ + { + name: "nil input", + src: nil, + want: nil, + }, + { + name: "empty slice", + src: []namespacedv1alpha2.Mapping{}, + want: []clusterv1alpha2.Mapping{}, + }, + { + name: "single mapping", + src: []namespacedv1alpha2.Mapping{ + { + Method: "GET", + URL: "http://test.com", + Body: "request body", + Headers: map[string][]string{ + "Authorization": {"Bearer token"}, + }, + }, + }, + want: []clusterv1alpha2.Mapping{ + { + Method: "GET", + URL: "http://test.com", + Body: "request body", + Headers: map[string][]string{ + "Authorization": {"Bearer token"}, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertNamespacedToClusterMappings(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertNamespacedToClusterMappings() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestConvertNamespacedToClusterDisposableRequestParameters(t *testing.T) { + timeout := &metav1.Duration{Duration: 10 * time.Minute} + nextReconcile := &metav1.Duration{Duration: 30 * time.Second} + + tests := []struct { + name string + src *namespaceddisposablev1alpha2.DisposableRequestParameters + want *clusterdisposablev1alpha2.DisposableRequestParameters + }{ + { + name: "nil input", + src: nil, + want: nil, + }, + { + name: "basic conversion", + src: &namespaceddisposablev1alpha2.DisposableRequestParameters{ + URL: "http://example.com/webhook", + Method: "POST", + Body: `{"message": "test"}`, + WaitTimeout: timeout, + NextReconcile: nextReconcile, + ShouldLoopInfinitely: false, + SecretInjectionConfigs: []common.SecretInjectionConfig{ + { + SecretRef: common.SecretRef{ + Name: "test-secret", + Namespace: "default", + }, + }, + }, + }, + want: &clusterdisposablev1alpha2.DisposableRequestParameters{ + URL: "http://example.com/webhook", + Method: "POST", + Body: `{"message": "test"}`, + WaitTimeout: timeout, + NextReconcile: nextReconcile, + ShouldLoopInfinitely: false, + SecretInjectionConfigs: []common.SecretInjectionConfig{ + { + SecretRef: common.SecretRef{ + Name: "test-secret", + Namespace: "default", + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertNamespacedToClusterDisposableRequestParameters(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertNamespacedToClusterDisposableRequestParameters() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestConvertNamespacedToClusterPayload(t *testing.T) { + tests := []struct { + name string + src namespacedv1alpha2.Payload + want clusterv1alpha2.Payload + }{ + { + name: "empty payload", + src: namespacedv1alpha2.Payload{}, + want: clusterv1alpha2.Payload{}, + }, + { + name: "payload with baseUrl and body", + src: namespacedv1alpha2.Payload{ + BaseUrl: "https://api.example.com", + Body: `{"key": "value"}`, + }, + want: clusterv1alpha2.Payload{ + BaseUrl: "https://api.example.com", + Body: `{"key": "value"}`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertNamespacedToClusterPayload(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertNamespacedToClusterPayload() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestConvertNamespacedToClusterExpectedResponseCheck(t *testing.T) { + tests := []struct { + name string + src namespacedv1alpha2.ExpectedResponseCheck + want clusterv1alpha2.ExpectedResponseCheck + }{ + { + name: "empty check", + src: namespacedv1alpha2.ExpectedResponseCheck{}, + want: clusterv1alpha2.ExpectedResponseCheck{}, + }, + { + name: "check with type and logic", + src: namespacedv1alpha2.ExpectedResponseCheck{ + Type: "jq", + Logic: ".status == \"success\"", + }, + want: clusterv1alpha2.ExpectedResponseCheck{ + Type: "jq", + Logic: ".status == \"success\"", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertNamespacedToClusterExpectedResponseCheck(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertNamespacedToClusterExpectedResponseCheck() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestConvertNamespacedToClusterResponse(t *testing.T) { + tests := []struct { + name string + src namespacedv1alpha2.Response + want clusterv1alpha2.Response + }{ + { + name: "empty response", + src: namespacedv1alpha2.Response{}, + want: clusterv1alpha2.Response{}, + }, + { + name: "complete response", + src: namespacedv1alpha2.Response{ + StatusCode: 200, + Body: `{"result": "success"}`, + Headers: map[string][]string{ + "Content-Type": {"application/json"}, + }, + }, + want: clusterv1alpha2.Response{ + StatusCode: 200, + Body: `{"result": "success"}`, + Headers: map[string][]string{ + "Content-Type": {"application/json"}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertNamespacedToClusterResponse(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertNamespacedToClusterResponse() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestConvertNamespacedToClusterMappingStatus(t *testing.T) { + tests := []struct { + name string + src namespacedv1alpha2.Mapping + want clusterv1alpha2.Mapping + }{ + { + name: "empty mapping", + src: namespacedv1alpha2.Mapping{}, + want: clusterv1alpha2.Mapping{}, + }, + { + name: "complete mapping", + src: namespacedv1alpha2.Mapping{ + Method: "PUT", + URL: "https://api.test.com/update", + Body: `{"update": true}`, + Headers: map[string][]string{ + "Authorization": {"Bearer xyz"}, + }, + }, + want: clusterv1alpha2.Mapping{ + Method: "PUT", + URL: "https://api.test.com/update", + Body: `{"update": true}`, + Headers: map[string][]string{ + "Authorization": {"Bearer xyz"}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertNamespacedToClusterMappingStatus(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertNamespacedToClusterMappingStatus() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestConvertNamespacedToClusterCache(t *testing.T) { + tests := []struct { + name string + src namespacedv1alpha2.Cache + want clusterv1alpha2.Cache + }{ + { + name: "empty cache", + src: namespacedv1alpha2.Cache{}, + want: clusterv1alpha2.Cache{}, + }, + { + name: "cache with response and timestamp", + src: namespacedv1alpha2.Cache{ + Response: namespacedv1alpha2.Response{ + StatusCode: 200, + Body: `{"cached": true}`, + }, + LastUpdated: "2023-01-01T12:00:00Z", + }, + want: clusterv1alpha2.Cache{ + Response: clusterv1alpha2.Response{ + StatusCode: 200, + Body: `{"cached": true}`, + }, + LastUpdated: "2023-01-01T12:00:00Z", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertNamespacedToClusterCache(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertNamespacedToClusterCache() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestConvertClusterToNamespacedResponse(t *testing.T) { + tests := []struct { + name string + src clusterv1alpha2.Response + want namespacedv1alpha2.Response + }{ + { + name: "empty response", + src: clusterv1alpha2.Response{}, + want: namespacedv1alpha2.Response{}, + }, + { + name: "complete response", + src: clusterv1alpha2.Response{ + StatusCode: 201, + Body: `{"created": true}`, + Headers: map[string][]string{ + "Location": {"https://api.example.com/resource/123"}, + }, + }, + want: namespacedv1alpha2.Response{ + StatusCode: 201, + Body: `{"created": true}`, + Headers: map[string][]string{ + "Location": {"https://api.example.com/resource/123"}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertClusterToNamespacedResponse(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertClusterToNamespacedResponse() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestConvertNamespacedToClusterDisposableResponse(t *testing.T) { + tests := []struct { + name string + src namespaceddisposablev1alpha2.Response + want clusterdisposablev1alpha2.Response + }{ + { + name: "empty response", + src: namespaceddisposablev1alpha2.Response{}, + want: clusterdisposablev1alpha2.Response{}, + }, + { + name: "complete response", + src: namespaceddisposablev1alpha2.Response{ + StatusCode: 202, + Body: `{"accepted": true}`, + Headers: map[string][]string{ + "X-Request-ID": {"req-123"}, + }, + }, + want: clusterdisposablev1alpha2.Response{ + StatusCode: 202, + Body: `{"accepted": true}`, + Headers: map[string][]string{ + "X-Request-ID": {"req-123"}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertNamespacedToClusterDisposableResponse(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertNamespacedToClusterDisposableResponse() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestConvertNamespacedToClusterDisposableMapping(t *testing.T) { + tests := []struct { + name string + src namespaceddisposablev1alpha2.Mapping + want clusterdisposablev1alpha2.Mapping + }{ + { + name: "empty mapping", + src: namespaceddisposablev1alpha2.Mapping{}, + want: clusterdisposablev1alpha2.Mapping{}, + }, + { + name: "complete mapping", + src: namespaceddisposablev1alpha2.Mapping{ + Method: "DELETE", + URL: "https://api.test.com/resource/456", + Body: "", + Headers: map[string][]string{ + "X-API-Key": {"secret-key"}, + }, + }, + want: clusterdisposablev1alpha2.Mapping{ + Method: "DELETE", + URL: "https://api.test.com/resource/456", + Body: "", + Headers: map[string][]string{ + "X-API-Key": {"secret-key"}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertNamespacedToClusterDisposableMapping(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertNamespacedToClusterDisposableMapping() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestConvertClusterToNamespacedDisposableResponse(t *testing.T) { + tests := []struct { + name string + src clusterdisposablev1alpha2.Response + want namespaceddisposablev1alpha2.Response + }{ + { + name: "empty response", + src: clusterdisposablev1alpha2.Response{}, + want: namespaceddisposablev1alpha2.Response{}, + }, + { + name: "complete response", + src: clusterdisposablev1alpha2.Response{ + StatusCode: 204, + Body: "", + Headers: map[string][]string{ + "X-Rate-Limit": {"100"}, + }, + }, + want: namespaceddisposablev1alpha2.Response{ + StatusCode: 204, + Body: "", + Headers: map[string][]string{ + "X-Rate-Limit": {"100"}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertClusterToNamespacedDisposableResponse(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertClusterToNamespacedDisposableResponse() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestConvertNamespacedToClusterRequest(t *testing.T) { + tests := []struct { + name string + src *namespacedv1alpha2.Request + want *clusterv1alpha2.Request + }{ + { + name: "nil input", + src: nil, + want: nil, + }, + { + name: "basic request conversion", + src: &namespacedv1alpha2.Request{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "http.m.crossplane.io/v1alpha2", + Kind: "Request", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-request", + Namespace: "default", + }, + Spec: namespacedv1alpha2.RequestSpec{ + ForProvider: namespacedv1alpha2.RequestParameters{ + Mappings: []namespacedv1alpha2.Mapping{ + { + Method: "POST", + URL: "https://example.com/api", + Body: `{"test": true}`, + }, + }, + }, + }, + Status: namespacedv1alpha2.RequestStatus{ + Failed: 0, + }, + }, + want: &clusterv1alpha2.Request{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "http.m.crossplane.io/v1alpha2", + Kind: "Request", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-request", + Namespace: "default", + }, + Spec: clusterv1alpha2.RequestSpec{ + ForProvider: clusterv1alpha2.RequestParameters{ + Mappings: []clusterv1alpha2.Mapping{ + { + Method: "POST", + URL: "https://example.com/api", + Body: `{"test": true}`, + }, + }, + }, + }, + Status: clusterv1alpha2.RequestStatus{ + Failed: 0, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertNamespacedToClusterRequest(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertNamespacedToClusterRequest() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestConvertNamespacedToClusterDisposableRequest(t *testing.T) { + tests := []struct { + name string + src *namespaceddisposablev1alpha2.DisposableRequest + want *clusterdisposablev1alpha2.DisposableRequest + }{ + { + name: "nil input", + src: nil, + want: nil, + }, + { + name: "basic disposable request conversion", + src: &namespaceddisposablev1alpha2.DisposableRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "http.m.crossplane.io/v1alpha2", + Kind: "DisposableRequest", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disposable", + Namespace: "test-ns", + }, + Spec: namespaceddisposablev1alpha2.DisposableRequestSpec{ + ForProvider: namespaceddisposablev1alpha2.DisposableRequestParameters{ + URL: "https://webhook.example.com", + Method: "POST", + Body: `{"event": "test"}`, + }, + }, + Status: namespaceddisposablev1alpha2.DisposableRequestStatus{ + Synced: true, + }, + }, + want: &clusterdisposablev1alpha2.DisposableRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "http.m.crossplane.io/v1alpha2", + Kind: "DisposableRequest", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disposable", + Namespace: "test-ns", + }, + Spec: clusterdisposablev1alpha2.DisposableRequestSpec{ + ForProvider: clusterdisposablev1alpha2.DisposableRequestParameters{ + URL: "https://webhook.example.com", + Method: "POST", + Body: `{"event": "test"}`, + }, + }, + Status: clusterdisposablev1alpha2.DisposableRequestStatus{ + Synced: true, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertNamespacedToClusterDisposableRequest(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertNamespacedToClusterDisposableRequest() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/data-patcher/patch.go b/internal/data-patcher/patch.go index b055759..3ccecbe 100644 --- a/internal/data-patcher/patch.go +++ b/internal/data-patcher/patch.go @@ -4,8 +4,8 @@ import ( "context" "fmt" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" "github.com/crossplane-contrib/provider-http/apis/common" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" kubehandler "github.com/crossplane-contrib/provider-http/internal/kube-handler" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" diff --git a/internal/json/util_test.go b/internal/json/util_test.go index 68def0a..ae9fb56 100644 --- a/internal/json/util_test.go +++ b/internal/json/util_test.go @@ -3,7 +3,7 @@ package json import ( "testing" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" "github.com/google/go-cmp/cmp" ) diff --git a/internal/utils/set_status_test.go b/internal/utils/set_status_test.go index ea172f4..3642ff7 100644 --- a/internal/utils/set_status_test.go +++ b/internal/utils/set_status_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - v1alpha1_disposable "github.com/crossplane-contrib/provider-http/apis/disposablerequest/v1alpha2" - v1alpha1_request "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + v1alpha1_disposable "github.com/crossplane-contrib/provider-http/apis/cluster/disposablerequest/v1alpha2" + v1alpha1_request "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/pkg/errors" diff --git a/package/crds/http.m.crossplane.io_clusterproviderconfigs.yaml b/package/crds/http.m.crossplane.io_clusterproviderconfigs.yaml new file mode 100644 index 0000000..9150dfe --- /dev/null +++ b/package/crds/http.m.crossplane.io_clusterproviderconfigs.yaml @@ -0,0 +1,170 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: clusterproviderconfigs.http.m.crossplane.io +spec: + group: http.m.crossplane.io + names: + kind: ClusterProviderConfig + listKind: ClusterProviderConfigList + plural: clusterproviderconfigs + singular: clusterproviderconfig + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + - jsonPath: .spec.credentials.secretRef.name + name: SECRET-NAME + priority: 1 + type: string + name: v1alpha2 + schema: + openAPIV3Schema: + description: A ClusterProviderConfig configures a Http provider for cross-namespace + access. + 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: A ProviderConfigSpec defines the desired state of a ProviderConfig. + properties: + credentials: + description: Credentials required to authenticate to this provider. + properties: + env: + description: |- + Env is a reference to an environment variable that contains credentials + that must be used to connect to the provider. + properties: + name: + description: Name is the name of an environment variable. + type: string + required: + - name + type: object + fs: + description: |- + Fs is a reference to a filesystem location that contains credentials that + must be used to connect to the provider. + properties: + path: + description: Path is a filesystem path. + type: string + required: + - path + type: object + secretRef: + description: |- + A SecretRef is a reference to a secret key that contains the credentials + that must be used to connect to the provider. + properties: + key: + description: The key to select. + type: string + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - key + - name + - namespace + type: object + source: + description: Source of the provider credentials. + enum: + - None + - Secret + - InjectedIdentity + - Environment + - Filesystem + type: string + required: + - source + type: object + required: + - credentials + type: object + status: + description: A ProviderConfigStatus reflects the observed state of a ProviderConfig. + properties: + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastTransitionTime: + description: |- + LastTransitionTime is the last time this condition transitioned from one + status to another. + format: date-time + type: string + message: + description: |- + A Message containing details about this condition's last transition from + one status to another, if any. + 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 + type: integer + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown? + type: string + type: + description: |- + Type of this condition. At most one of each condition type may apply to + a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + users: + description: Users of this provider configuration. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/package/crds/http.m.crossplane.io_clusterproviderconfigusages.yaml b/package/crds/http.m.crossplane.io_clusterproviderconfigusages.yaml new file mode 100644 index 0000000..05298bd --- /dev/null +++ b/package/crds/http.m.crossplane.io_clusterproviderconfigusages.yaml @@ -0,0 +1,118 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: clusterproviderconfigusages.http.m.crossplane.io +spec: + group: http.m.crossplane.io + names: + categories: + - crossplane + - provider + - http + kind: ClusterProviderConfigUsage + listKind: ClusterProviderConfigUsageList + plural: clusterproviderconfigusages + singular: clusterproviderconfigusage + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + - jsonPath: .providerConfigRef.name + name: CONFIG-NAME + type: string + - jsonPath: .resourceRef.kind + name: RESOURCE-KIND + type: string + - jsonPath: .resourceRef.name + name: RESOURCE-NAME + type: string + name: v1alpha2 + schema: + openAPIV3Schema: + description: A ClusterProviderConfigUsage indicates that a resource is using + a ClusterProviderConfig. + 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 + providerConfigRef: + description: ProviderConfigReference to the provider config being used. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + resourceRef: + description: ResourceReference to the managed resource using the provider + config. + properties: + apiVersion: + description: APIVersion of the referenced object. + type: string + kind: + description: Kind of the referenced object. + type: string + name: + description: Name of the referenced object. + type: string + uid: + description: UID of the referenced object. + type: string + required: + - apiVersion + - kind + - name + type: object + required: + - providerConfigRef + - resourceRef + type: object + served: true + storage: true + subresources: {} diff --git a/package/crds/http.m.crossplane.io_disposablerequests.yaml b/package/crds/http.m.crossplane.io_disposablerequests.yaml new file mode 100644 index 0000000..a6e574d --- /dev/null +++ b/package/crds/http.m.crossplane.io_disposablerequests.yaml @@ -0,0 +1,417 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: disposablerequests.http.m.crossplane.io +spec: + group: http.m.crossplane.io + names: + categories: + - crossplane + - managed + - http + kind: DisposableRequest + listKind: DisposableRequestList + plural: disposablerequests + singular: disposablerequest + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: READY + type: string + - jsonPath: .status.conditions[?(@.type=='Synced')].status + name: SYNCED + type: string + - jsonPath: .metadata.annotations.crossplane\.io/external-name + name: EXTERNAL-NAME + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: A DisposableRequest is a namespaced HTTP disposable request resource. + 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: A DisposableRequestSpec defines the desired state of a DisposableRequest. + properties: + deletionPolicy: + default: Delete + description: |- + DeletionPolicy specifies what will happen to the underlying external + when this managed resource is deleted - either "Delete" or "Orphan" the + external resource. + This field is planned to be deprecated in favor of the ManagementPolicies + field in a future release. Currently, both could be set independently and + non-default values would be honored if the feature flag is enabled. + See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 + enum: + - Orphan + - Delete + type: string + forProvider: + description: DisposableRequestParameters are the configurable fields + of a DisposableRequest. + properties: + body: + type: string + x-kubernetes-validations: + - message: Field 'forProvider.body' is immutable + rule: self == oldSelf + expectedResponse: + description: |- + ExpectedResponse is a jq filter expression used to evaluate the HTTP response and determine if it matches the expected criteria. + The expression should return a boolean; if true, the response is considered expected. + Example: '.body.job_status == "success"' + type: string + headers: + additionalProperties: + items: + type: string + type: array + type: object + x-kubernetes-validations: + - message: Field 'forProvider.headers' is immutable + rule: self == oldSelf + insecureSkipTLSVerify: + description: InsecureSkipTLSVerify, when set to true, skips TLS + certificate checks for the HTTP request + type: boolean + method: + type: string + x-kubernetes-validations: + - message: Field 'forProvider.method' is immutable + rule: self == oldSelf + nextReconcile: + description: NextReconcile specifies the duration after which + the next reconcile should occur. + type: string + rollbackRetriesLimit: + description: RollbackRetriesLimit is max number of attempts to + retry HTTP request by sending again the request. + format: int32 + type: integer + secretInjectionConfigs: + description: SecretInjectionConfig specifies the secrets receiving + patches from response data. + items: + description: SecretInjectionConfig represents the configuration + for injecting secret data into a Kubernetes secret. + properties: + keyMappings: + description: KeyMappings allows injecting data into single + or multiple keys within the same Kubernetes secret. + items: + description: KeyInjection represents the configuration + for injecting data into a specific key in a Kubernetes + secret. + properties: + missingFieldStrategy: + default: delete + description: |- + MissingFieldStrategy determines how to handle cases where the field is missing from the response. + Possible values are: + - "preserve": keeps the existing value in the secret + - "setEmpty": sets the value to the empty string + - "delete": removes the key from the s + enum: + - preserve + - setEmpty + - delete + type: string + responseJQ: + description: ResponseJQ is a jq filter expression + representing the path in the response where the + secret value will be extracted from. + type: string + secretKey: + description: SecretKey is the key within the Kubernetes + secret where the data will be injected. + type: string + required: + - responseJQ + - secretKey + type: object + type: array + metadata: + description: Metadata contains labels and annotations to + apply to the Kubernetes secret. + properties: + annotations: + additionalProperties: + type: string + description: Annotations contains key-value pairs to + apply as annotations to the Kubernetes secret. + type: object + labels: + additionalProperties: + type: string + description: Labels contains key-value pairs to apply + as labels to the Kubernetes secret. + type: object + type: object + responsePath: + description: |- + ResponsePath is a jq filter expression representing the path in the response where the secret value will be extracted from. + Deprecated: Use KeyMappings for injecting single or multiple keys. + type: string + secretKey: + description: |- + SecretKey is the key within the Kubernetes secret where the data will be injected. + Deprecated: Use KeyMappings for injecting single or multiple keys. + type: string + secretRef: + description: SecretRef contains the name and namespace of + the Kubernetes secret where the data will be injected. + properties: + name: + description: Name is the name of the Kubernetes secret. + type: string + namespace: + description: Namespace is the namespace of the Kubernetes + secret. + type: string + required: + - name + - namespace + type: object + setOwnerReference: + description: SetOwnerReference determines whether to set + the owner reference on the Kubernetes secret. + type: boolean + required: + - secretRef + type: object + type: array + shouldLoopInfinitely: + description: ShouldLoopInfinitely specifies whether the reconciliation + should loop indefinitely. + type: boolean + url: + type: string + x-kubernetes-validations: + - message: Field 'forProvider.url' is immutable + rule: self == oldSelf + waitTimeout: + description: WaitTimeout specifies the maximum time duration for + waiting. + type: string + required: + - method + - url + type: object + managementPolicies: + default: + - '*' + description: |- + THIS IS A BETA FIELD. It is on by default but can be opted out + through a Crossplane feature flag. + ManagementPolicies specify the array of actions Crossplane is allowed to + take on the managed and external resources. + This field is planned to replace the DeletionPolicy field in a future + release. Currently, both could be set independently and non-default + values would be honored if the feature flag is enabled. If both are + custom, the DeletionPolicy field will be ignored. + See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 + and this one: https://github.com/crossplane/crossplane/blob/444267e84783136daa93568b364a5f01228cacbe/design/one-pager-ignore-changes.md + items: + description: |- + A ManagementAction represents an action that the Crossplane controllers + can take on an external resource. + enum: + - Observe + - Create + - Update + - Delete + - LateInitialize + - '*' + type: string + type: array + providerConfigRef: + default: + name: default + description: |- + ProviderConfigReference specifies how the provider that will be used to + create, observe, update, and delete this managed resource should be + configured. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + writeConnectionSecretToRef: + description: |- + WriteConnectionSecretToReference specifies the namespace and name of a + Secret to which any connection details for this managed resource should + be written. Connection details frequently include the endpoint, username, + and password required to connect to the managed resource. + properties: + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - name + - namespace + type: object + required: + - forProvider + type: object + status: + description: A DisposableRequestStatus represents the observed state of + a DisposableRequest. + properties: + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastTransitionTime: + description: |- + LastTransitionTime is the last time this condition transitioned from one + status to another. + format: date-time + type: string + message: + description: |- + A Message containing details about this condition's last transition from + one status to another, if any. + 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 + type: integer + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown? + type: string + type: + description: |- + Type of this condition. At most one of each condition type may apply to + a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + error: + type: string + failed: + format: int32 + type: integer + lastReconcileTime: + description: LastReconcileTime records the last time the resource + was reconciled. + format: date-time + type: string + observedGeneration: + description: |- + ObservedGeneration is the latest metadata.generation + which resulted in either a ready state, or stalled due to error + it can not recover from without human intervention. + format: int64 + type: integer + requestDetails: + properties: + body: + type: string + headers: + additionalProperties: + items: + type: string + type: array + type: object + method: + type: string + url: + type: string + required: + - method + - url + type: object + response: + properties: + body: + type: string + headers: + additionalProperties: + items: + type: string + type: array + type: object + statusCode: + type: integer + type: object + synced: + type: boolean + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/package/crds/http.m.crossplane.io_providerconfigs.yaml b/package/crds/http.m.crossplane.io_providerconfigs.yaml new file mode 100644 index 0000000..7c190c2 --- /dev/null +++ b/package/crds/http.m.crossplane.io_providerconfigs.yaml @@ -0,0 +1,169 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: providerconfigs.http.m.crossplane.io +spec: + group: http.m.crossplane.io + names: + kind: ProviderConfig + listKind: ProviderConfigList + plural: providerconfigs + singular: providerconfig + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + - jsonPath: .spec.credentials.secretRef.name + name: SECRET-NAME + priority: 1 + type: string + name: v1alpha2 + schema: + openAPIV3Schema: + description: A ProviderConfig configures a Http provider for namespaced resources. + 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: A ProviderConfigSpec defines the desired state of a ProviderConfig. + properties: + credentials: + description: Credentials required to authenticate to this provider. + properties: + env: + description: |- + Env is a reference to an environment variable that contains credentials + that must be used to connect to the provider. + properties: + name: + description: Name is the name of an environment variable. + type: string + required: + - name + type: object + fs: + description: |- + Fs is a reference to a filesystem location that contains credentials that + must be used to connect to the provider. + properties: + path: + description: Path is a filesystem path. + type: string + required: + - path + type: object + secretRef: + description: |- + A SecretRef is a reference to a secret key that contains the credentials + that must be used to connect to the provider. + properties: + key: + description: The key to select. + type: string + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - key + - name + - namespace + type: object + source: + description: Source of the provider credentials. + enum: + - None + - Secret + - InjectedIdentity + - Environment + - Filesystem + type: string + required: + - source + type: object + required: + - credentials + type: object + status: + description: A ProviderConfigStatus reflects the observed state of a ProviderConfig. + properties: + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastTransitionTime: + description: |- + LastTransitionTime is the last time this condition transitioned from one + status to another. + format: date-time + type: string + message: + description: |- + A Message containing details about this condition's last transition from + one status to another, if any. + 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 + type: integer + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown? + type: string + type: + description: |- + Type of this condition. At most one of each condition type may apply to + a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + users: + description: Users of this provider configuration. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/package/crds/http.m.crossplane.io_providerconfigusages.yaml b/package/crds/http.m.crossplane.io_providerconfigusages.yaml new file mode 100644 index 0000000..e55b5cc --- /dev/null +++ b/package/crds/http.m.crossplane.io_providerconfigusages.yaml @@ -0,0 +1,117 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: providerconfigusages.http.m.crossplane.io +spec: + group: http.m.crossplane.io + names: + categories: + - crossplane + - provider + - http + kind: ProviderConfigUsage + listKind: ProviderConfigUsageList + plural: providerconfigusages + singular: providerconfigusage + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + - jsonPath: .providerConfigRef.name + name: CONFIG-NAME + type: string + - jsonPath: .resourceRef.kind + name: RESOURCE-KIND + type: string + - jsonPath: .resourceRef.name + name: RESOURCE-NAME + type: string + name: v1alpha2 + schema: + openAPIV3Schema: + description: A ProviderConfigUsage indicates that a resource is using a ProviderConfig. + 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 + providerConfigRef: + description: ProviderConfigReference to the provider config being used. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + resourceRef: + description: ResourceReference to the managed resource using the provider + config. + properties: + apiVersion: + description: APIVersion of the referenced object. + type: string + kind: + description: Kind of the referenced object. + type: string + name: + description: Name of the referenced object. + type: string + uid: + description: UID of the referenced object. + type: string + required: + - apiVersion + - kind + - name + type: object + required: + - providerConfigRef + - resourceRef + type: object + served: true + storage: true + subresources: {} diff --git a/package/crds/http.m.crossplane.io_requests.yaml b/package/crds/http.m.crossplane.io_requests.yaml new file mode 100644 index 0000000..4a00634 --- /dev/null +++ b/package/crds/http.m.crossplane.io_requests.yaml @@ -0,0 +1,499 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: requests.http.m.crossplane.io +spec: + group: http.m.crossplane.io + names: + categories: + - crossplane + - managed + - http + kind: Request + listKind: RequestList + plural: requests + singular: request + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: READY + type: string + - jsonPath: .status.conditions[?(@.type=='Synced')].status + name: SYNCED + type: string + - jsonPath: .metadata.annotations.crossplane\.io/external-name + name: EXTERNAL-NAME + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: A Request is a namespaced HTTP request resource. + 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: A RequestSpec defines the desired state of a Request. + properties: + deletionPolicy: + default: Delete + description: |- + DeletionPolicy specifies what will happen to the underlying external + when this managed resource is deleted - either "Delete" or "Orphan" the + external resource. + This field is planned to be deprecated in favor of the ManagementPolicies + field in a future release. Currently, both could be set independently and + non-default values would be honored if the feature flag is enabled. + See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 + enum: + - Orphan + - Delete + type: string + forProvider: + description: RequestParameters are the configurable fields of a Request. + properties: + expectedResponseCheck: + description: ExpectedResponseCheck specifies the mechanism to + validate the OBSERVE response against expected value. + properties: + logic: + description: Logic specifies the custom logic for the expected + response check. + type: string + type: + description: Type specifies the type of the expected response + check. + enum: + - DEFAULT + - CUSTOM + type: string + type: object + headers: + additionalProperties: + items: + type: string + type: array + description: Headers defines default headers for each request. + type: object + insecureSkipTLSVerify: + description: InsecureSkipTLSVerify, when set to true, skips TLS + certificate checks for the HTTP request + type: boolean + isRemovedCheck: + description: IsRemovedCheck specifies the mechanism to validate + the OBSERVE response after removal against expected value. + properties: + logic: + description: Logic specifies the custom logic for the expected + response check. + type: string + type: + description: Type specifies the type of the expected response + check. + enum: + - DEFAULT + - CUSTOM + type: string + type: object + mappings: + description: |- + Mappings defines the HTTP mappings for different methods. + Either Method or Action must be specified. If both are omitted, the mapping will not be used. + items: + properties: + action: + description: Action specifies the intended action for the + request. + enum: + - CREATE + - OBSERVE + - UPDATE + - REMOVE + type: string + body: + description: Body specifies the body of the request. + type: string + headers: + additionalProperties: + items: + type: string + type: array + description: Headers specifies the headers for the request. + type: object + method: + description: Method specifies the HTTP method for the request. + enum: + - POST + - GET + - PUT + - DELETE + - PATCH + - HEAD + - OPTIONS + type: string + url: + description: URL specifies the URL for the request. + type: string + required: + - url + type: object + minItems: 1 + type: array + payload: + description: Payload defines the payload for the request. + properties: + baseUrl: + description: BaseUrl specifies the base URL for the request. + type: string + body: + description: Body specifies data to be used in the request + body. + type: string + type: object + secretInjectionConfigs: + description: SecretInjectionConfig specifies the secrets receiving + patches for response data. + items: + description: SecretInjectionConfig represents the configuration + for injecting secret data into a Kubernetes secret. + properties: + keyMappings: + description: KeyMappings allows injecting data into single + or multiple keys within the same Kubernetes secret. + items: + description: KeyInjection represents the configuration + for injecting data into a specific key in a Kubernetes + secret. + properties: + missingFieldStrategy: + default: delete + description: |- + MissingFieldStrategy determines how to handle cases where the field is missing from the response. + Possible values are: + - "preserve": keeps the existing value in the secret + - "setEmpty": sets the value to the empty string + - "delete": removes the key from the s + enum: + - preserve + - setEmpty + - delete + type: string + responseJQ: + description: ResponseJQ is a jq filter expression + representing the path in the response where the + secret value will be extracted from. + type: string + secretKey: + description: SecretKey is the key within the Kubernetes + secret where the data will be injected. + type: string + required: + - responseJQ + - secretKey + type: object + type: array + metadata: + description: Metadata contains labels and annotations to + apply to the Kubernetes secret. + properties: + annotations: + additionalProperties: + type: string + description: Annotations contains key-value pairs to + apply as annotations to the Kubernetes secret. + type: object + labels: + additionalProperties: + type: string + description: Labels contains key-value pairs to apply + as labels to the Kubernetes secret. + type: object + type: object + responsePath: + description: |- + ResponsePath is a jq filter expression representing the path in the response where the secret value will be extracted from. + Deprecated: Use KeyMappings for injecting single or multiple keys. + type: string + secretKey: + description: |- + SecretKey is the key within the Kubernetes secret where the data will be injected. + Deprecated: Use KeyMappings for injecting single or multiple keys. + type: string + secretRef: + description: SecretRef contains the name and namespace of + the Kubernetes secret where the data will be injected. + properties: + name: + description: Name is the name of the Kubernetes secret. + type: string + namespace: + description: Namespace is the namespace of the Kubernetes + secret. + type: string + required: + - name + - namespace + type: object + setOwnerReference: + description: SetOwnerReference determines whether to set + the owner reference on the Kubernetes secret. + type: boolean + required: + - secretRef + type: object + type: array + waitTimeout: + description: WaitTimeout specifies the maximum time duration for + waiting. + type: string + required: + - mappings + - payload + type: object + managementPolicies: + default: + - '*' + description: |- + THIS IS A BETA FIELD. It is on by default but can be opted out + through a Crossplane feature flag. + ManagementPolicies specify the array of actions Crossplane is allowed to + take on the managed and external resources. + This field is planned to replace the DeletionPolicy field in a future + release. Currently, both could be set independently and non-default + values would be honored if the feature flag is enabled. If both are + custom, the DeletionPolicy field will be ignored. + See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 + and this one: https://github.com/crossplane/crossplane/blob/444267e84783136daa93568b364a5f01228cacbe/design/one-pager-ignore-changes.md + items: + description: |- + A ManagementAction represents an action that the Crossplane controllers + can take on an external resource. + enum: + - Observe + - Create + - Update + - Delete + - LateInitialize + - '*' + type: string + type: array + providerConfigRef: + default: + name: default + description: |- + ProviderConfigReference specifies how the provider that will be used to + create, observe, update, and delete this managed resource should be + configured. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + writeConnectionSecretToRef: + description: |- + WriteConnectionSecretToReference specifies the namespace and name of a + Secret to which any connection details for this managed resource should + be written. Connection details frequently include the endpoint, username, + and password required to connect to the managed resource. + properties: + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - name + - namespace + type: object + required: + - forProvider + type: object + status: + description: A RequestStatus represents the observed state of a Request. + properties: + cache: + properties: + lastUpdated: + type: string + response: + description: RequestObservation are the observable fields of a + Request. + properties: + body: + type: string + headers: + additionalProperties: + items: + type: string + type: array + type: object + statusCode: + type: integer + type: object + type: object + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastTransitionTime: + description: |- + LastTransitionTime is the last time this condition transitioned from one + status to another. + format: date-time + type: string + message: + description: |- + A Message containing details about this condition's last transition from + one status to another, if any. + 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 + type: integer + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown? + type: string + type: + description: |- + Type of this condition. At most one of each condition type may apply to + a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + error: + type: string + failed: + format: int32 + type: integer + observedGeneration: + description: |- + ObservedGeneration is the latest metadata.generation + which resulted in either a ready state, or stalled due to error + it can not recover from without human intervention. + format: int64 + type: integer + requestDetails: + properties: + action: + description: Action specifies the intended action for the request. + enum: + - CREATE + - OBSERVE + - UPDATE + - REMOVE + type: string + body: + description: Body specifies the body of the request. + type: string + headers: + additionalProperties: + items: + type: string + type: array + description: Headers specifies the headers for the request. + type: object + method: + description: Method specifies the HTTP method for the request. + enum: + - POST + - GET + - PUT + - DELETE + - PATCH + - HEAD + - OPTIONS + type: string + url: + description: URL specifies the URL for the request. + type: string + required: + - url + type: object + response: + description: RequestObservation are the observable fields of a Request. + properties: + body: + type: string + headers: + additionalProperties: + items: + type: string + type: array + type: object + statusCode: + type: integer + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/package/crossplane.yaml b/package/crossplane.yaml index eab1fe0..065af78 100644 --- a/package/crossplane.yaml +++ b/package/crossplane.yaml @@ -20,3 +20,6 @@ metadata: join our community discussions on [slack.crossplane.io](https://slack.crossplane.io). Feel free to create issues or contribute to the development at [crossplane-contrib/provider-http](https://github.com/crossplane-contrib/provider-http). +spec: + capabilities: + - safe-start From ffb8b2fb327e73d63d73e665c08ffac5e6d488a3 Mon Sep 17 00:00:00 2001 From: Riccardo Capraro Date: Wed, 22 Oct 2025 15:18:36 +0200 Subject: [PATCH 3/9] Fix e2e tests for namespaced resources --- Makefile | 9 +- cluster/test/setup.sh | 50 +++++++++ examples/namespaced/README.md | 45 +++++--- .../namespaced/clusterproviderconfig.yaml | 7 -- examples/namespaced/providerconfig.yaml | 8 -- .../namespaced/config/clusterconfig.go | 106 +++++++++--------- 6 files changed, 142 insertions(+), 83 deletions(-) delete mode 100644 examples/namespaced/clusterproviderconfig.yaml delete mode 100644 examples/namespaced/providerconfig.yaml diff --git a/Makefile b/Makefile index 91c880b..9ecd7c0 100644 --- a/Makefile +++ b/Makefile @@ -90,11 +90,16 @@ CROSSPLANE_NAMESPACE = crossplane-system -include build/makelib/local.xpkg.mk -include build/makelib/controlplane.mk -UPTEST_EXAMPLE_LIST := $(shell find ./examples/sample -path '*.yaml' | paste -s -d ',' - ) +# Conditionally include namespaced examples for Crossplane v2 +ifeq ($(shell echo "$(CROSSPLANE_VERSION)" | cut -d. -f1),2) + UPTEST_EXAMPLE_LIST := $(shell find ./examples/sample -path '*.yaml' | paste -s -d ',' - ),$(shell find ./examples/namespaced -path '*.yaml' | paste -s -d ',' - ) +else + UPTEST_EXAMPLE_LIST := $(shell find ./examples/sample -path '*.yaml' | paste -s -d ',' - ) +endif uptest: $(UPTEST) $(KUBECTL) $(CHAINSAW) $(CROSSPLANE_CLI) @$(INFO) running automated tests - @KUBECTL=$(KUBECTL) CHAINSAW=$(CHAINSAW) CROSSPLANE_CLI=$(CROSSPLANE_CLI) CROSSPLANE_NAMESPACE=$(CROSSPLANE_NAMESPACE) $(UPTEST) e2e "$(UPTEST_EXAMPLE_LIST)" --setup-script=cluster/test/setup.sh || $(FAIL) + @KUBECTL=$(KUBECTL) CHAINSAW=$(CHAINSAW) CROSSPLANE_CLI=$(CROSSPLANE_CLI) CROSSPLANE_NAMESPACE=$(CROSSPLANE_NAMESPACE) CROSSPLANE_VERSION=$(CROSSPLANE_VERSION) $(UPTEST) e2e "$(UPTEST_EXAMPLE_LIST)" --setup-script=cluster/test/setup.sh || $(FAIL) @$(OK) running automated tests local-dev: controlplane.up diff --git a/cluster/test/setup.sh b/cluster/test/setup.sh index bf49b78..0baa184 100755 --- a/cluster/test/setup.sh +++ b/cluster/test/setup.sh @@ -16,6 +16,56 @@ spec: source: None EOF +# Check if we're running Crossplane v2 and create namespaced provider configurations +if [ -z "${CROSSPLANE_VERSION:-}" ]; then + echo "ERROR: CROSSPLANE_VERSION environment variable must be set" + exit 1 +fi +MAJOR_VERSION=$(echo "$CROSSPLANE_VERSION" | cut -d. -f1) + +if [ "$MAJOR_VERSION" = "2" ]; then + echo "Detected Crossplane v2, creating namespaced provider configurations..." + + # Create namespaced ProviderConfig + cat < Date: Wed, 22 Oct 2025 23:28:37 +0200 Subject: [PATCH 4/9] Fix DisposableRequest deletion and cross-namespace owner reference issues - Fix DisposableRequest deletion logic to skip secret injection during deletion - Fix cross-namespace owner reference validation in data-patcher to prevent errors - Add unit tests for DisposableRequest namespaced controller - Update example configuration to remove invalid setOwnerReference for cross-namespace secrets - Add data-patcher tests for cross-namespace scenarios - Fix secret name mismatch in request-with-clusterproviderconfig.yaml example Signed-off-by: Riccardo Capraro --- .../v1alpha1/zz_generated.managed.go | 1 - .../v1alpha1/zz_generated.managedlist.go | 29 ++ .../v1alpha2/zz_generated.managed.go | 1 - .../v1alpha2/zz_generated.managedlist.go | 29 ++ .../request/v1alpha1/zz_generated.managed.go | 1 - .../v1alpha1/zz_generated.managedlist.go | 29 ++ .../request/v1alpha2/zz_generated.managed.go | 1 - .../v1alpha2/zz_generated.managedlist.go | 29 ++ apis/cluster/v1alpha1/zz_generated.pcu.go | 30 ++ apis/generate.go | 9 - .../v1alpha2/disposablerequest_types.go | 5 +- .../v1alpha2/zz_generated.deepcopy.go | 2 +- .../v1alpha2/zz_generated.managed.go | 19 +- .../v1alpha2/zz_generated.managedlist.go | 29 ++ .../request/v1alpha2/request_types.go | 5 +- .../request/v1alpha2/zz_generated.deepcopy.go | 2 +- .../request/v1alpha2/zz_generated.managed.go | 19 +- .../v1alpha2/zz_generated.managedlist.go | 29 ++ .../v1alpha2/providerconfig_types.go | 2 +- apis/namespaced/v1alpha2/zz_generated.pcu.go | 40 +++ cluster/test/setup.sh | 67 +++- .../namespaced/disposablerequest-jwt.yaml | 2 +- ...blerequest-with-clusterproviderconfig.yaml | 9 +- examples/namespaced/disposablerequest.yaml | 2 +- .../request-with-clusterproviderconfig.yaml | 5 +- examples/namespaced/request.yaml | 3 +- go.mod | 8 +- go.sum | 16 +- .../disposablerequest/disposablerequest.go | 52 ++- .../disposablerequest_test.go | 338 ++++++++++++++++++ .../controller/namespaced/request/request.go | 46 ++- internal/controller/typeconv/conversion.go | 40 ++- .../controller/typeconv/conversion_test.go | 230 ++++++++++++ internal/data-patcher/patch.go | 8 +- internal/data-patcher/patch_test.go | 144 ++++++++ ...tp.m.crossplane.io_disposablerequests.yaml | 52 +-- .../crds/http.m.crossplane.io_requests.yaml | 52 +-- 37 files changed, 1182 insertions(+), 203 deletions(-) create mode 100644 apis/cluster/disposablerequest/v1alpha1/zz_generated.managedlist.go create mode 100644 apis/cluster/disposablerequest/v1alpha2/zz_generated.managedlist.go create mode 100644 apis/cluster/request/v1alpha1/zz_generated.managedlist.go create mode 100644 apis/cluster/request/v1alpha2/zz_generated.managedlist.go create mode 100644 apis/cluster/v1alpha1/zz_generated.pcu.go create mode 100644 apis/namespaced/disposablerequest/v1alpha2/zz_generated.managedlist.go create mode 100644 apis/namespaced/request/v1alpha2/zz_generated.managedlist.go create mode 100644 apis/namespaced/v1alpha2/zz_generated.pcu.go create mode 100644 internal/controller/namespaced/disposablerequest/disposablerequest_test.go diff --git a/apis/cluster/disposablerequest/v1alpha1/zz_generated.managed.go b/apis/cluster/disposablerequest/v1alpha1/zz_generated.managed.go index f533d43..f4f7245 100644 --- a/apis/cluster/disposablerequest/v1alpha1/zz_generated.managed.go +++ b/apis/cluster/disposablerequest/v1alpha1/zz_generated.managed.go @@ -13,7 +13,6 @@ 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. */ - // Code generated by angryjet. DO NOT EDIT. package v1alpha1 diff --git a/apis/cluster/disposablerequest/v1alpha1/zz_generated.managedlist.go b/apis/cluster/disposablerequest/v1alpha1/zz_generated.managedlist.go new file mode 100644 index 0000000..4fc6c1e --- /dev/null +++ b/apis/cluster/disposablerequest/v1alpha1/zz_generated.managedlist.go @@ -0,0 +1,29 @@ +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha1 + +import resource "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + +// GetItems of this DisposableRequestList. +func (l *DisposableRequestList) GetItems() []resource.Managed { + items := make([]resource.Managed, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items +} diff --git a/apis/cluster/disposablerequest/v1alpha2/zz_generated.managed.go b/apis/cluster/disposablerequest/v1alpha2/zz_generated.managed.go index 08ed963..952f331 100644 --- a/apis/cluster/disposablerequest/v1alpha2/zz_generated.managed.go +++ b/apis/cluster/disposablerequest/v1alpha2/zz_generated.managed.go @@ -13,7 +13,6 @@ 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. */ - // Code generated by angryjet. DO NOT EDIT. package v1alpha2 diff --git a/apis/cluster/disposablerequest/v1alpha2/zz_generated.managedlist.go b/apis/cluster/disposablerequest/v1alpha2/zz_generated.managedlist.go new file mode 100644 index 0000000..9f555c0 --- /dev/null +++ b/apis/cluster/disposablerequest/v1alpha2/zz_generated.managedlist.go @@ -0,0 +1,29 @@ +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha2 + +import resource "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + +// GetItems of this DisposableRequestList. +func (l *DisposableRequestList) GetItems() []resource.Managed { + items := make([]resource.Managed, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items +} diff --git a/apis/cluster/request/v1alpha1/zz_generated.managed.go b/apis/cluster/request/v1alpha1/zz_generated.managed.go index 040f4db..45b09e1 100644 --- a/apis/cluster/request/v1alpha1/zz_generated.managed.go +++ b/apis/cluster/request/v1alpha1/zz_generated.managed.go @@ -13,7 +13,6 @@ 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. */ - // Code generated by angryjet. DO NOT EDIT. package v1alpha1 diff --git a/apis/cluster/request/v1alpha1/zz_generated.managedlist.go b/apis/cluster/request/v1alpha1/zz_generated.managedlist.go new file mode 100644 index 0000000..fc7fb54 --- /dev/null +++ b/apis/cluster/request/v1alpha1/zz_generated.managedlist.go @@ -0,0 +1,29 @@ +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha1 + +import resource "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + +// GetItems of this RequestList. +func (l *RequestList) GetItems() []resource.Managed { + items := make([]resource.Managed, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items +} diff --git a/apis/cluster/request/v1alpha2/zz_generated.managed.go b/apis/cluster/request/v1alpha2/zz_generated.managed.go index aca48e0..0459767 100644 --- a/apis/cluster/request/v1alpha2/zz_generated.managed.go +++ b/apis/cluster/request/v1alpha2/zz_generated.managed.go @@ -13,7 +13,6 @@ 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. */ - // Code generated by angryjet. DO NOT EDIT. package v1alpha2 diff --git a/apis/cluster/request/v1alpha2/zz_generated.managedlist.go b/apis/cluster/request/v1alpha2/zz_generated.managedlist.go new file mode 100644 index 0000000..58d7864 --- /dev/null +++ b/apis/cluster/request/v1alpha2/zz_generated.managedlist.go @@ -0,0 +1,29 @@ +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha2 + +import resource "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + +// GetItems of this RequestList. +func (l *RequestList) GetItems() []resource.Managed { + items := make([]resource.Managed, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items +} diff --git a/apis/cluster/v1alpha1/zz_generated.pcu.go b/apis/cluster/v1alpha1/zz_generated.pcu.go new file mode 100644 index 0000000..8651bf0 --- /dev/null +++ b/apis/cluster/v1alpha1/zz_generated.pcu.go @@ -0,0 +1,30 @@ +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha1 + +import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + +// GetProviderConfigReference of this ProviderConfigUsage. +func (p *ProviderConfigUsage) GetProviderConfigReference() xpv1.Reference { + return p.ProviderConfigReference +} + +// SetProviderConfigReference of this ProviderConfigUsage. +func (p *ProviderConfigUsage) SetProviderConfigReference(r xpv1.Reference) { + p.ProviderConfigReference = r +} diff --git a/apis/generate.go b/apis/generate.go index 8dc4f86..3ed8e3f 100644 --- a/apis/generate.go +++ b/apis/generate.go @@ -1,6 +1,3 @@ -//go:build generate -// +build generate - /* Copyright 2020 The Crossplane Authors. @@ -30,9 +27,3 @@ limitations under the License. //go:generate go run -tags generate github.com/crossplane/crossplane-tools/cmd/angryjet generate-methodsets --header-file=../hack/boilerplate.go.txt ./... package apis - -import ( - _ "sigs.k8s.io/controller-tools/cmd/controller-gen" //nolint:typecheck - - _ "github.com/crossplane/crossplane-tools/cmd/angryjet" //nolint:typecheck -) diff --git a/apis/namespaced/disposablerequest/v1alpha2/disposablerequest_types.go b/apis/namespaced/disposablerequest/v1alpha2/disposablerequest_types.go index 5bd97e7..c476f46 100644 --- a/apis/namespaced/disposablerequest/v1alpha2/disposablerequest_types.go +++ b/apis/namespaced/disposablerequest/v1alpha2/disposablerequest_types.go @@ -24,6 +24,7 @@ import ( "github.com/crossplane-contrib/provider-http/apis/common" xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + xpv2 "github.com/crossplane/crossplane-runtime/v2/apis/common/v2" ) // DisposableRequestParameters are the configurable fields of a DisposableRequest. @@ -63,8 +64,8 @@ type DisposableRequestParameters struct { // A DisposableRequestSpec defines the desired state of a DisposableRequest. type DisposableRequestSpec struct { - xpv1.ResourceSpec `json:",inline"` - ForProvider DisposableRequestParameters `json:"forProvider"` + xpv2.ManagedResourceSpec `json:",inline"` + ForProvider DisposableRequestParameters `json:"forProvider"` } type Response struct { diff --git a/apis/namespaced/disposablerequest/v1alpha2/zz_generated.deepcopy.go b/apis/namespaced/disposablerequest/v1alpha2/zz_generated.deepcopy.go index d5f1e0a..92aefbb 100644 --- a/apis/namespaced/disposablerequest/v1alpha2/zz_generated.deepcopy.go +++ b/apis/namespaced/disposablerequest/v1alpha2/zz_generated.deepcopy.go @@ -141,7 +141,7 @@ func (in *DisposableRequestParameters) DeepCopy() *DisposableRequestParameters { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DisposableRequestSpec) DeepCopyInto(out *DisposableRequestSpec) { *out = *in - in.ResourceSpec.DeepCopyInto(&out.ResourceSpec) + in.ManagedResourceSpec.DeepCopyInto(&out.ManagedResourceSpec) in.ForProvider.DeepCopyInto(&out.ForProvider) } diff --git a/apis/namespaced/disposablerequest/v1alpha2/zz_generated.managed.go b/apis/namespaced/disposablerequest/v1alpha2/zz_generated.managed.go index 08ed963..cbbf70f 100644 --- a/apis/namespaced/disposablerequest/v1alpha2/zz_generated.managed.go +++ b/apis/namespaced/disposablerequest/v1alpha2/zz_generated.managed.go @@ -13,7 +13,6 @@ 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. */ - // Code generated by angryjet. DO NOT EDIT. package v1alpha2 @@ -25,23 +24,18 @@ func (mg *DisposableRequest) GetCondition(ct xpv1.ConditionType) xpv1.Condition return mg.Status.GetCondition(ct) } -// GetDeletionPolicy of this DisposableRequest. -func (mg *DisposableRequest) GetDeletionPolicy() xpv1.DeletionPolicy { - return mg.Spec.DeletionPolicy -} - // GetManagementPolicies of this DisposableRequest. func (mg *DisposableRequest) GetManagementPolicies() xpv1.ManagementPolicies { return mg.Spec.ManagementPolicies } // GetProviderConfigReference of this DisposableRequest. -func (mg *DisposableRequest) GetProviderConfigReference() *xpv1.Reference { +func (mg *DisposableRequest) GetProviderConfigReference() *xpv1.ProviderConfigReference { return mg.Spec.ProviderConfigReference } // GetWriteConnectionSecretToReference of this DisposableRequest. -func (mg *DisposableRequest) GetWriteConnectionSecretToReference() *xpv1.SecretReference { +func (mg *DisposableRequest) GetWriteConnectionSecretToReference() *xpv1.LocalSecretReference { return mg.Spec.WriteConnectionSecretToReference } @@ -50,22 +44,17 @@ func (mg *DisposableRequest) SetConditions(c ...xpv1.Condition) { mg.Status.SetConditions(c...) } -// SetDeletionPolicy of this DisposableRequest. -func (mg *DisposableRequest) SetDeletionPolicy(r xpv1.DeletionPolicy) { - mg.Spec.DeletionPolicy = r -} - // SetManagementPolicies of this DisposableRequest. func (mg *DisposableRequest) SetManagementPolicies(r xpv1.ManagementPolicies) { mg.Spec.ManagementPolicies = r } // SetProviderConfigReference of this DisposableRequest. -func (mg *DisposableRequest) SetProviderConfigReference(r *xpv1.Reference) { +func (mg *DisposableRequest) SetProviderConfigReference(r *xpv1.ProviderConfigReference) { mg.Spec.ProviderConfigReference = r } // SetWriteConnectionSecretToReference of this DisposableRequest. -func (mg *DisposableRequest) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { +func (mg *DisposableRequest) SetWriteConnectionSecretToReference(r *xpv1.LocalSecretReference) { mg.Spec.WriteConnectionSecretToReference = r } diff --git a/apis/namespaced/disposablerequest/v1alpha2/zz_generated.managedlist.go b/apis/namespaced/disposablerequest/v1alpha2/zz_generated.managedlist.go new file mode 100644 index 0000000..9f555c0 --- /dev/null +++ b/apis/namespaced/disposablerequest/v1alpha2/zz_generated.managedlist.go @@ -0,0 +1,29 @@ +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha2 + +import resource "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + +// GetItems of this DisposableRequestList. +func (l *DisposableRequestList) GetItems() []resource.Managed { + items := make([]resource.Managed, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items +} diff --git a/apis/namespaced/request/v1alpha2/request_types.go b/apis/namespaced/request/v1alpha2/request_types.go index 2833320..fd02fa5 100644 --- a/apis/namespaced/request/v1alpha2/request_types.go +++ b/apis/namespaced/request/v1alpha2/request_types.go @@ -24,6 +24,7 @@ import ( "github.com/crossplane-contrib/provider-http/apis/common" xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + xpv2 "github.com/crossplane/crossplane-runtime/v2/apis/common/v2" ) const ( @@ -105,8 +106,8 @@ type Payload struct { // A RequestSpec defines the desired state of a Request. type RequestSpec struct { - xpv1.ResourceSpec `json:",inline"` - ForProvider RequestParameters `json:"forProvider"` + xpv2.ManagedResourceSpec `json:",inline"` + ForProvider RequestParameters `json:"forProvider"` } // RequestObservation are the observable fields of a Request. diff --git a/apis/namespaced/request/v1alpha2/zz_generated.deepcopy.go b/apis/namespaced/request/v1alpha2/zz_generated.deepcopy.go index 54c2e9a..0467d78 100644 --- a/apis/namespaced/request/v1alpha2/zz_generated.deepcopy.go +++ b/apis/namespaced/request/v1alpha2/zz_generated.deepcopy.go @@ -218,7 +218,7 @@ func (in *RequestParameters) DeepCopy() *RequestParameters { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RequestSpec) DeepCopyInto(out *RequestSpec) { *out = *in - in.ResourceSpec.DeepCopyInto(&out.ResourceSpec) + in.ManagedResourceSpec.DeepCopyInto(&out.ManagedResourceSpec) in.ForProvider.DeepCopyInto(&out.ForProvider) } diff --git a/apis/namespaced/request/v1alpha2/zz_generated.managed.go b/apis/namespaced/request/v1alpha2/zz_generated.managed.go index aca48e0..6d066f6 100644 --- a/apis/namespaced/request/v1alpha2/zz_generated.managed.go +++ b/apis/namespaced/request/v1alpha2/zz_generated.managed.go @@ -13,7 +13,6 @@ 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. */ - // Code generated by angryjet. DO NOT EDIT. package v1alpha2 @@ -25,23 +24,18 @@ func (mg *Request) GetCondition(ct xpv1.ConditionType) xpv1.Condition { return mg.Status.GetCondition(ct) } -// GetDeletionPolicy of this Request. -func (mg *Request) GetDeletionPolicy() xpv1.DeletionPolicy { - return mg.Spec.DeletionPolicy -} - // GetManagementPolicies of this Request. func (mg *Request) GetManagementPolicies() xpv1.ManagementPolicies { return mg.Spec.ManagementPolicies } // GetProviderConfigReference of this Request. -func (mg *Request) GetProviderConfigReference() *xpv1.Reference { +func (mg *Request) GetProviderConfigReference() *xpv1.ProviderConfigReference { return mg.Spec.ProviderConfigReference } // GetWriteConnectionSecretToReference of this Request. -func (mg *Request) GetWriteConnectionSecretToReference() *xpv1.SecretReference { +func (mg *Request) GetWriteConnectionSecretToReference() *xpv1.LocalSecretReference { return mg.Spec.WriteConnectionSecretToReference } @@ -50,22 +44,17 @@ func (mg *Request) SetConditions(c ...xpv1.Condition) { mg.Status.SetConditions(c...) } -// SetDeletionPolicy of this Request. -func (mg *Request) SetDeletionPolicy(r xpv1.DeletionPolicy) { - mg.Spec.DeletionPolicy = r -} - // SetManagementPolicies of this Request. func (mg *Request) SetManagementPolicies(r xpv1.ManagementPolicies) { mg.Spec.ManagementPolicies = r } // SetProviderConfigReference of this Request. -func (mg *Request) SetProviderConfigReference(r *xpv1.Reference) { +func (mg *Request) SetProviderConfigReference(r *xpv1.ProviderConfigReference) { mg.Spec.ProviderConfigReference = r } // SetWriteConnectionSecretToReference of this Request. -func (mg *Request) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { +func (mg *Request) SetWriteConnectionSecretToReference(r *xpv1.LocalSecretReference) { mg.Spec.WriteConnectionSecretToReference = r } diff --git a/apis/namespaced/request/v1alpha2/zz_generated.managedlist.go b/apis/namespaced/request/v1alpha2/zz_generated.managedlist.go new file mode 100644 index 0000000..58d7864 --- /dev/null +++ b/apis/namespaced/request/v1alpha2/zz_generated.managedlist.go @@ -0,0 +1,29 @@ +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha2 + +import resource "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + +// GetItems of this RequestList. +func (l *RequestList) GetItems() []resource.Managed { + items := make([]resource.Managed, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items +} diff --git a/apis/namespaced/v1alpha2/providerconfig_types.go b/apis/namespaced/v1alpha2/providerconfig_types.go index c849740..9754188 100644 --- a/apis/namespaced/v1alpha2/providerconfig_types.go +++ b/apis/namespaced/v1alpha2/providerconfig_types.go @@ -166,4 +166,4 @@ func (pcul *ProviderConfigUsageList) GetItems() []resource.ProviderConfigUsage { func init() { SchemeBuilder.Register(&ProviderConfig{}, &ProviderConfigList{}) SchemeBuilder.Register(&ProviderConfigUsage{}, &ProviderConfigUsageList{}) -} +} \ No newline at end of file diff --git a/apis/namespaced/v1alpha2/zz_generated.pcu.go b/apis/namespaced/v1alpha2/zz_generated.pcu.go new file mode 100644 index 0000000..9c6d649 --- /dev/null +++ b/apis/namespaced/v1alpha2/zz_generated.pcu.go @@ -0,0 +1,40 @@ +/* +Copyright 2020 The Crossplane Authors. + +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. +*/ +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha2 + +import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + +// GetProviderConfigReference of this ClusterProviderConfigUsage. +func (p *ClusterProviderConfigUsage) GetProviderConfigReference() xpv1.Reference { + return p.ProviderConfigReference +} + +// SetProviderConfigReference of this ClusterProviderConfigUsage. +func (p *ClusterProviderConfigUsage) SetProviderConfigReference(r xpv1.Reference) { + p.ProviderConfigReference = r +} + +// GetProviderConfigReference of this ProviderConfigUsage. +func (p *ProviderConfigUsage) GetProviderConfigReference() xpv1.Reference { + return p.ProviderConfigReference +} + +// SetProviderConfigReference of this ProviderConfigUsage. +func (p *ProviderConfigUsage) SetProviderConfigReference(r xpv1.Reference) { + p.ProviderConfigReference = r +} diff --git a/cluster/test/setup.sh b/cluster/test/setup.sh index 0baa184..2337076 100755 --- a/cluster/test/setup.sh +++ b/cluster/test/setup.sh @@ -60,6 +60,71 @@ type: Opaque data: token: bXktc2VjcmV0LXZhbHVl EOF + + # Create user-password secret in default namespace for namespaced examples + cat </dev/null 2>&1; do + sleep 1 + done + echo "Secret $name in namespace $namespace is available" + done + + # Additional wait to ensure provider has processed the secrets + echo "Waiting 10 seconds for provider to process secrets..." + sleep 10 echo "Namespaced provider configurations created successfully" else @@ -75,7 +140,7 @@ metadata: labels: app: flask-api spec: - replicas: 3 + replicas: 1 selector: matchLabels: app: flask-api diff --git a/examples/namespaced/disposablerequest-jwt.yaml b/examples/namespaced/disposablerequest-jwt.yaml index 253680e..5ded3bc 100644 --- a/examples/namespaced/disposablerequest-jwt.yaml +++ b/examples/namespaced/disposablerequest-jwt.yaml @@ -4,7 +4,6 @@ metadata: name: obtain-jwt-token-namespaced namespace: default spec: - deletionPolicy: Orphan forProvider: insecureSkipTLSVerify: true @@ -34,3 +33,4 @@ spec: setOwnerReference: true providerConfigRef: name: http-conf-namespaced + kind: ProviderConfig diff --git a/examples/namespaced/disposablerequest-with-clusterproviderconfig.yaml b/examples/namespaced/disposablerequest-with-clusterproviderconfig.yaml index 1a6c2a8..04520cc 100644 --- a/examples/namespaced/disposablerequest-with-clusterproviderconfig.yaml +++ b/examples/namespaced/disposablerequest-with-clusterproviderconfig.yaml @@ -4,7 +4,6 @@ metadata: name: send-notification-with-cluster-config namespace: default spec: - deletionPolicy: Orphan forProvider: # This example demonstrates using ClusterProviderConfig with namespaced DisposableRequest # ClusterProviderConfig allows cross-namespace access to secrets and configurations @@ -53,9 +52,11 @@ spec: - secretKey: notification-id responseJQ: .body.id missingFieldStrategy: preserve - # setOwnerReference determines if the secret should be deleted when the associated resource is deleted. - setOwnerReference: true + # setOwnerReference must be false for cross-namespace secrets due to Kubernetes restrictions + # Cross-namespace owner references are disallowed by Kubernetes + setOwnerReference: false # Using ClusterProviderConfig for cross-namespace access providerConfigRef: - name: http-conf-cluster \ No newline at end of file + name: http-conf-cluster + kind: ClusterProviderConfig \ No newline at end of file diff --git a/examples/namespaced/disposablerequest.yaml b/examples/namespaced/disposablerequest.yaml index 3e4706b..a49838a 100644 --- a/examples/namespaced/disposablerequest.yaml +++ b/examples/namespaced/disposablerequest.yaml @@ -4,7 +4,6 @@ metadata: name: send-notification-namespaced namespace: default spec: - deletionPolicy: Orphan forProvider: # Injecting data from secrets is possible, simply use the following syntax: {{ name:namespace:key }} (supported for body and headers only) url: http://flask-api.default.svc.cluster.local/v1/notify @@ -56,3 +55,4 @@ spec: providerConfigRef: name: http-conf-namespaced + kind: ProviderConfig diff --git a/examples/namespaced/request-with-clusterproviderconfig.yaml b/examples/namespaced/request-with-clusterproviderconfig.yaml index dc1dd58..8b08deb 100644 --- a/examples/namespaced/request-with-clusterproviderconfig.yaml +++ b/examples/namespaced/request-with-clusterproviderconfig.yaml @@ -69,7 +69,7 @@ spec: if .response.body.password == .payload.body.password and .response.body.age == 35 and .response.headers."Content-Type" == ["application/json"] - and .response.headers."X-Secret-Header"[0] == "{{ response-secret:crossplane-system:extracted-header-data }}" + and .response.headers."X-Secret-Header"[0] == "{{ response-secret-cluster:crossplane-system:extracted-header-data }}" then true else false end @@ -115,4 +115,5 @@ spec: # Using ClusterProviderConfig instead of namespaced ProviderConfig # This allows cross-namespace access for secrets and configurations providerConfigRef: - name: http-conf-cluster \ No newline at end of file + name: http-conf-cluster + kind: ClusterProviderConfig \ No newline at end of file diff --git a/examples/namespaced/request.yaml b/examples/namespaced/request.yaml index 4fe19a5..87e9ad3 100644 --- a/examples/namespaced/request.yaml +++ b/examples/namespaced/request.yaml @@ -125,4 +125,5 @@ spec: responseJQ: .body.age providerConfigRef: - name: http-conf-namespaced \ No newline at end of file + name: http-conf-namespaced + kind: ProviderConfig \ No newline at end of file diff --git a/go.mod b/go.mod index d14b8c7..4e90223 100644 --- a/go.mod +++ b/go.mod @@ -6,19 +6,16 @@ toolchain go1.24.6 require ( github.com/crossplane/crossplane-runtime/v2 v2.0.0 - github.com/crossplane/crossplane-tools v0.0.0-20240522174801-1ad3d4c87f21 github.com/google/go-cmp v0.7.0 github.com/pkg/errors v0.9.1 gopkg.in/alecthomas/kingpin.v2 v2.2.6 k8s.io/apimachinery v0.34.1 k8s.io/client-go v0.34.1 sigs.k8s.io/controller-runtime v0.21.0 - sigs.k8s.io/controller-tools v0.18.0 ) require ( dario.cat/mergo v1.0.1 // indirect - github.com/alecthomas/kingpin/v2 v2.4.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect @@ -27,7 +24,6 @@ require ( github.com/itchyny/timefmt-go v0.1.6 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.opentelemetry.io/otel v1.33.0 // indirect go.opentelemetry.io/otel/trace v1.33.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect @@ -38,16 +34,16 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect k8s.io/code-generator v0.33.0 // indirect k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f // indirect + sigs.k8s.io/controller-tools v0.18.0 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect ) require ( github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect - github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect + github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/dave/jennifer v1.7.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect diff --git a/go.sum b/go.sum index 8170a5e..3d610f5 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,9 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= -github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= -github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -16,10 +14,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/crossplane/crossplane-runtime/v2 v2.0.0 h1:PK2pTKfshdDZ5IfoiMRiCi0PBnIjqbS0KGXEJgRdrb4= github.com/crossplane/crossplane-runtime/v2 v2.0.0/go.mod h1:pkd5UzmE8esaZAApevMutR832GjJ1Qgc5Ngr78ByxrI= -github.com/crossplane/crossplane-tools v0.0.0-20240522174801-1ad3d4c87f21 h1:8wb7/zCbVPkeX68WbVESWJmSWQE5SZKzz0g9X4FlXRw= -github.com/crossplane/crossplane-tools v0.0.0-20240522174801-1ad3d4c87f21/go.mod h1:cN0Y7PFGQMM8mcagXVCbeQoKtipmFWQTPZYyziCPBUI= -github.com/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE= -github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -142,16 +136,15 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= -github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= @@ -234,7 +227,6 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/controller/namespaced/disposablerequest/disposablerequest.go b/internal/controller/namespaced/disposablerequest/disposablerequest.go index bccfe20..76cfffe 100644 --- a/internal/controller/namespaced/disposablerequest/disposablerequest.go +++ b/internal/controller/namespaced/disposablerequest/disposablerequest.go @@ -62,6 +62,10 @@ const ( errExtractCredentials = "cannot extract credentials" errCheckExpectedResponse = "failed to check if response is as expected" errResponseDoesntMatchExpectedCriteria = "response does not match expected criteria" + + errGetPC = "cannot get ProviderConfig" + errGetCPC = "cannot get ClusterProviderConfig" + errGetCreds = "cannot get credentials" ) // Setup adds a controller that reconciles namespaced DisposableRequest managed resources. @@ -106,30 +110,42 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E // Set default providerConfigRef if not specified if cr.GetProviderConfigReference() == nil { - cr.SetProviderConfigReference(&xpv1.Reference{ + cr.SetProviderConfigReference(&xpv1.ProviderConfigReference{ Name: "default", + Kind: "ClusterProviderConfig", }) l.Debug("No providerConfigRef specified, defaulting to 'default'") } - // Get the namespaced ProviderConfig - pc := &apisv1alpha2.ProviderConfig{} - n := types.NamespacedName{Name: cr.GetProviderConfigReference().Name, Namespace: cr.Namespace} - if err := c.kube.Get(ctx, n, pc); err != nil { - return nil, errors.Wrap(err, errProviderNotRetrieved) - } + var cd apisv1alpha2.ProviderCredentials + + // Switch to ModernManaged resource to get ProviderConfigRef + m := mg.(resource.ModernManaged) + ref := m.GetProviderConfigReference() - creds := "" - if pc.Spec.Credentials.Source == xpv1.CredentialsSourceSecret { - data, err := resource.CommonCredentialExtractor(ctx, pc.Spec.Credentials.Source, c.kube, pc.Spec.Credentials.CommonCredentialSelectors) - if err != nil { - return nil, errors.Wrap(err, errExtractCredentials) + switch ref.Kind { + case "ProviderConfig": + pc := &apisv1alpha2.ProviderConfig{} + if err := c.kube.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: m.GetNamespace()}, pc); err != nil { + return nil, errors.Wrap(err, errGetPC) } + cd = pc.Spec.Credentials + case "ClusterProviderConfig": + cpc := &apisv1alpha2.ClusterProviderConfig{} + if err := c.kube.Get(ctx, types.NamespacedName{Name: ref.Name}, cpc); err != nil { + return nil, errors.Wrap(err, errGetCPC) + } + cd = cpc.Spec.Credentials + default: + return nil, errors.Errorf("unsupported provider config kind: %s", ref.Kind) + } - creds = string(data) + data, err := resource.CommonCredentialExtractor(ctx, cd.Source, c.kube, cd.CommonCredentialSelectors) + if err != nil { + return nil, errors.Wrap(err, errExtractCredentials) } - h, err := c.newHttpClientFn(l, utils.WaitTimeout(cr.Spec.ForProvider.WaitTimeout), creds) + h, err := c.newHttpClientFn(l, utils.WaitTimeout(cr.Spec.ForProvider.WaitTimeout), string(data)) if err != nil { return nil, errors.Wrap(err, errNewHttpClient) } @@ -154,6 +170,14 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex return managed.ExternalObservation{}, errors.New(errNotNamespacedDisposableRequest) } + // Skip secret injection processing if the resource is being deleted + if cr.GetDeletionTimestamp() != nil { + c.logger.Debug("DisposableRequest is being deleted, skipping observation and secret injection") + return managed.ExternalObservation{ + ResourceExists: false, + }, nil + } + isUpToDate := !(utils.ShouldRetry(cr.Spec.ForProvider.RollbackRetriesLimit, cr.Status.Failed) && !utils.RetriesLimitReached(cr.Status.Failed, cr.Spec.ForProvider.RollbackRetriesLimit)) isAvailable := isUpToDate diff --git a/internal/controller/namespaced/disposablerequest/disposablerequest_test.go b/internal/controller/namespaced/disposablerequest/disposablerequest_test.go new file mode 100644 index 0000000..73a0340 --- /dev/null +++ b/internal/controller/namespaced/disposablerequest/disposablerequest_test.go @@ -0,0 +1,338 @@ +/* +Copyright 2023 The Crossplane Authors. + +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 disposablerequest + +import ( + "context" + "testing" + + "github.com/crossplane-contrib/provider-http/apis/namespaced/disposablerequest/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + errBoom = errors.New("boom") +) + +type MockHttpClient struct { + MockSendRequest func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) +} + +func (m *MockHttpClient) SendRequest(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + return m.MockSendRequest(ctx, method, url, body, headers, skipTLSVerify) +} + +type notNamespacedDisposableRequest struct { + resource.Managed +} + +func namespacedDisposableRequest(modifiers ...func(*v1alpha2.DisposableRequest)) *v1alpha2.DisposableRequest { + cr := &v1alpha2.DisposableRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disposable", + Namespace: "default", + }, + Spec: v1alpha2.DisposableRequestSpec{ + ForProvider: v1alpha2.DisposableRequestParameters{ + URL: "http://example.com/test", + Method: "POST", + Body: `{"test": true}`, + }, + }, + } + + for _, modifier := range modifiers { + modifier(cr) + } + + return cr +} + +func namespacedDisposableRequestWithDeletion() *v1alpha2.DisposableRequest { + now := metav1.Now() + return namespacedDisposableRequest(func(cr *v1alpha2.DisposableRequest) { + cr.DeletionTimestamp = &now + }) +} + + +func TestObserve(t *testing.T) { + type args struct { + http httpClient.Client + localKube client.Client + mg resource.Managed + } + type want struct { + obs managed.ExternalObservation + err error + } + + cases := []struct { + name string + args args + want want + }{ + { + name: "NotNamespacedDisposableRequest", + args: args{ + mg: notNamespacedDisposableRequest{}, + }, + want: want{ + err: errors.New(errNotNamespacedDisposableRequest), + }, + }, + { + name: "ResourceBeingDeleted", + args: args{ + mg: namespacedDisposableRequestWithDeletion(), + }, + want: want{ + obs: managed.ExternalObservation{ + ResourceExists: false, + }, + }, + }, + { + name: "ResourceNotSynced", + args: args{ + mg: namespacedDisposableRequest(), + }, + want: want{ + obs: managed.ExternalObservation{ + ResourceExists: false, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + e := &external{ + logger: logging.NewNopLogger(), + localKube: tc.args.localKube, + http: tc.args.http, + } + + got, err := e.Observe(context.Background(), tc.args.mg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("Observe(...): -want error, +got error: %s", diff) + } + if diff := cmp.Diff(tc.want.obs, got); diff != "" { + t.Errorf("Observe(...): -want, +got: %s", diff) + } + }) + } +} + +func TestCreate(t *testing.T) { + type args struct { + http httpClient.Client + localKube client.Client + mg resource.Managed + } + type want struct { + err error + } + + cases := []struct { + name string + args args + want want + }{ + { + name: "NotNamespacedDisposableRequest", + args: args{ + mg: notNamespacedDisposableRequest{}, + }, + want: want{ + err: errors.New(errNotNamespacedDisposableRequest), + }, + }, + { + name: "HttpRequestFailed", + args: args{ + http: &MockHttpClient{ + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + return httpClient.HttpDetails{}, errBoom + }, + }, + localKube: &test.MockClient{ + MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), + MockGet: test.NewMockGetFn(nil), + }, + mg: namespacedDisposableRequest(), + }, + want: want{ + err: errors.Wrap(errBoom, errFailedToSendHttpDisposableRequest), + }, + }, + { + name: "Success", + args: args{ + http: &MockHttpClient{ + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + return httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + StatusCode: 200, + Body: `{"result": "success"}`, + }, + }, nil + }, + }, + localKube: &test.MockClient{ + MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), + MockGet: test.NewMockGetFn(nil), + }, + mg: namespacedDisposableRequest(), + }, + want: want{ + err: nil, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + e := &external{ + logger: logging.NewNopLogger(), + localKube: tc.args.localKube, + http: tc.args.http, + } + + _, err := e.Create(context.Background(), tc.args.mg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("Create(...): -want error, +got error: %s", diff) + } + }) + } +} + +func TestUpdate(t *testing.T) { + type args struct { + http httpClient.Client + localKube client.Client + mg resource.Managed + } + type want struct { + err error + } + + cases := []struct { + name string + args args + want want + }{ + { + name: "NotNamespacedDisposableRequest", + args: args{ + mg: notNamespacedDisposableRequest{}, + }, + want: want{ + err: errors.New(errNotNamespacedDisposableRequest), + }, + }, + { + name: "Success", + args: args{ + http: &MockHttpClient{ + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + return httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + StatusCode: 200, + Body: `{"result": "updated"}`, + }, + }, nil + }, + }, + localKube: &test.MockClient{ + MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), + MockGet: test.NewMockGetFn(nil), + }, + mg: namespacedDisposableRequest(), + }, + want: want{ + err: nil, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + e := &external{ + logger: logging.NewNopLogger(), + localKube: tc.args.localKube, + http: tc.args.http, + } + + _, err := e.Update(context.Background(), tc.args.mg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("Update(...): -want error, +got error: %s", diff) + } + }) + } +} + +func TestDelete(t *testing.T) { + type args struct { + mg resource.Managed + } + type want struct { + result managed.ExternalDelete + err error + } + + cases := []struct { + name string + args args + want want + }{ + { + name: "Success", + args: args{ + mg: namespacedDisposableRequest(), + }, + want: want{ + result: managed.ExternalDelete{}, + err: nil, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + e := &external{ + logger: logging.NewNopLogger(), + } + + got, err := e.Delete(context.Background(), tc.args.mg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("Delete(...): -want error, +got error: %s", diff) + } + if diff := cmp.Diff(tc.want.result, got); diff != "" { + t.Errorf("Delete(...): -want result, +got result: %s", diff) + } + }) + } +} \ No newline at end of file diff --git a/internal/controller/namespaced/request/request.go b/internal/controller/namespaced/request/request.go index 3f18770..cc7d03f 100644 --- a/internal/controller/namespaced/request/request.go +++ b/internal/controller/namespaced/request/request.go @@ -57,6 +57,10 @@ const ( errPatchDataToSecret = "Warning, couldn't patch data from request to secret %s:%s:%s, error: %s" errGetLatestVersion = "failed to get the latest version of the resource" errExtractCredentials = "cannot extract credentials" + + errGetPC = "cannot get ProviderConfig" + errGetCPC = "cannot get ClusterProviderConfig" + errGetCreds = "cannot get credentials" ) // Setup adds a controller that reconciles namespaced Request managed resources. @@ -99,30 +103,42 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E // Set default providerConfigRef if not specified if cr.GetProviderConfigReference() == nil { - cr.SetProviderConfigReference(&xpv1.Reference{ + cr.SetProviderConfigReference(&xpv1.ProviderConfigReference{ Name: "default", + Kind: "ClusterProviderConfig", }) l.Debug("No providerConfigRef specified, defaulting to 'default'") } - // Get the namespaced ProviderConfig - pc := &apisv1alpha2.ProviderConfig{} - n := types.NamespacedName{Name: cr.GetProviderConfigReference().Name, Namespace: cr.Namespace} - if err := c.kube.Get(ctx, n, pc); err != nil { - return nil, errors.Wrap(err, errProviderNotRetrieved) - } + var cd apisv1alpha2.ProviderCredentials + + // Switch to ModernManaged resource to get ProviderConfigRef + m := mg.(resource.ModernManaged) + ref := m.GetProviderConfigReference() - creds := "" - if pc.Spec.Credentials.Source == xpv1.CredentialsSourceSecret { - data, err := resource.CommonCredentialExtractor(ctx, pc.Spec.Credentials.Source, c.kube, pc.Spec.Credentials.CommonCredentialSelectors) - if err != nil { - return nil, errors.Wrap(err, errExtractCredentials) + switch ref.Kind { + case "ProviderConfig": + pc := &apisv1alpha2.ProviderConfig{} + if err := c.kube.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: m.GetNamespace()}, pc); err != nil { + return nil, errors.Wrap(err, errGetPC) } + cd = pc.Spec.Credentials + case "ClusterProviderConfig": + cpc := &apisv1alpha2.ClusterProviderConfig{} + if err := c.kube.Get(ctx, types.NamespacedName{Name: ref.Name}, cpc); err != nil { + return nil, errors.Wrap(err, errGetCPC) + } + cd = cpc.Spec.Credentials + default: + return nil, errors.Errorf("unsupported provider config kind: %s", ref.Kind) + } - creds = string(data) + data, err := resource.CommonCredentialExtractor(ctx, cd.Source, c.kube, cd.CommonCredentialSelectors) + if err != nil { + return nil, errors.Wrap(err, errExtractCredentials) } - h, err := c.newHttpClientFn(l, utils.WaitTimeout(cr.Spec.ForProvider.WaitTimeout), creds) + h, err := c.newHttpClientFn(l, utils.WaitTimeout(cr.Spec.ForProvider.WaitTimeout), string(data)) if err != nil { return nil, errors.Wrap(err, errNewHttpClient) } @@ -188,6 +204,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { cr, ok := mg.(*v1alpha2.Request) + cr.Status.SetConditions(xpv1.Creating()) if !ok { return managed.ExternalCreation{}, errors.New(errNotRequest) } @@ -206,6 +223,7 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext func (c *external) Delete(ctx context.Context, mg resource.Managed) (managed.ExternalDelete, error) { cr, ok := mg.(*v1alpha2.Request) + cr.Status.SetConditions(xpv1.Deleting()) if !ok { return managed.ExternalDelete{}, errors.New(errNotRequest) } diff --git a/internal/controller/typeconv/conversion.go b/internal/controller/typeconv/conversion.go index fdbcfa1..62a4e74 100644 --- a/internal/controller/typeconv/conversion.go +++ b/internal/controller/typeconv/conversion.go @@ -22,6 +22,10 @@ import ( clusterv1alpha2 "github.com/crossplane-contrib/provider-http/apis/cluster/request/v1alpha2" namespaceddisposablev1alpha2 "github.com/crossplane-contrib/provider-http/apis/namespaced/disposablerequest/v1alpha2" namespacedv1alpha2 "github.com/crossplane-contrib/provider-http/apis/namespaced/request/v1alpha2" + + "github.com/crossplane/crossplane-runtime/v2/apis/common" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + xpv2 "github.com/crossplane/crossplane-runtime/v2/apis/common/v2" ) // ConvertNamespacedToClusterRequestParameters converts namespaced RequestParameters to cluster RequestParameters @@ -52,6 +56,7 @@ func ConvertNamespacedToClusterMappings(src []namespacedv1alpha2.Mapping) []clus for i, mapping := range src { result[i] = clusterv1alpha2.Mapping{ Method: mapping.Method, + Action: mapping.Action, Body: mapping.Body, URL: mapping.URL, Headers: mapping.Headers, @@ -86,7 +91,7 @@ func ConvertNamespacedToClusterRequest(src *namespacedv1alpha2.Request) *cluster TypeMeta: src.TypeMeta, ObjectMeta: src.ObjectMeta, Spec: clusterv1alpha2.RequestSpec{ - ResourceSpec: src.Spec.ResourceSpec, + ResourceSpec: ConvertManagedResourceSpecToResourceSpec(src.Spec.ManagedResourceSpec), ForProvider: *ConvertNamespacedToClusterRequestParameters(&src.Spec.ForProvider), }, Status: ConvertNamespacedToClusterRequestStatus(src.Status), @@ -118,6 +123,7 @@ func ConvertNamespacedToClusterResponse(src namespacedv1alpha2.Response) cluster func ConvertNamespacedToClusterMappingStatus(src namespacedv1alpha2.Mapping) clusterv1alpha2.Mapping { return clusterv1alpha2.Mapping{ Method: src.Method, + Action: src.Action, Body: src.Body, URL: src.URL, Headers: src.Headers, @@ -174,7 +180,7 @@ func ConvertNamespacedToClusterDisposableRequest(src *namespaceddisposablev1alph TypeMeta: src.TypeMeta, ObjectMeta: src.ObjectMeta, Spec: clusterdisposablev1alpha2.DisposableRequestSpec{ - ResourceSpec: src.Spec.ResourceSpec, + ResourceSpec: ConvertManagedResourceSpecToResourceSpec(src.Spec.ManagedResourceSpec), ForProvider: *ConvertNamespacedToClusterDisposableRequestParameters(&src.Spec.ForProvider), }, Status: ConvertNamespacedToClusterDisposableRequestStatus(src.Status), @@ -236,3 +242,33 @@ func ToClusterRequest(src *namespacedv1alpha2.Request) (*clusterv1alpha2.Request func ToClusterRequestActionObserve() string { return clusterv1alpha2.ActionObserve } + +// ConvertManagedResourceSpecToResourceSpec converts v2 ManagedResourceSpec to v1 ResourceSpec +func ConvertManagedResourceSpecToResourceSpec(src xpv2.ManagedResourceSpec) xpv1.ResourceSpec { + return xpv1.ResourceSpec{ + WriteConnectionSecretToReference: ConvertLocalSecretToSecretReference(src.WriteConnectionSecretToReference), + ProviderConfigReference: ConvertProviderConfigReferenceToReference(src.ProviderConfigReference), + ManagementPolicies: src.ManagementPolicies, + } +} + +// ConvertLocalSecretToSecretReference converts common.LocalSecretReference to v1.SecretReference +func ConvertLocalSecretToSecretReference(src *common.LocalSecretReference) *xpv1.SecretReference { + if src == nil { + return nil + } + return &xpv1.SecretReference{ + Name: src.Name, + // LocalSecretReference doesn't have Namespace - it's local to the managed resource's namespace + } +} + +// ConvertProviderConfigReferenceToReference converts common.ProviderConfigReference to v1.Reference +func ConvertProviderConfigReferenceToReference(src *common.ProviderConfigReference) *xpv1.Reference { + if src == nil { + return nil + } + return &xpv1.Reference{ + Name: src.Name, + } +} diff --git a/internal/controller/typeconv/conversion_test.go b/internal/controller/typeconv/conversion_test.go index 5f234f3..777605d 100644 --- a/internal/controller/typeconv/conversion_test.go +++ b/internal/controller/typeconv/conversion_test.go @@ -28,6 +28,10 @@ import ( "github.com/crossplane-contrib/provider-http/apis/common" namespaceddisposablev1alpha2 "github.com/crossplane-contrib/provider-http/apis/namespaced/disposablerequest/v1alpha2" namespacedv1alpha2 "github.com/crossplane-contrib/provider-http/apis/namespaced/request/v1alpha2" + + commonxp "github.com/crossplane/crossplane-runtime/v2/apis/common" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + xpv2 "github.com/crossplane/crossplane-runtime/v2/apis/common/v2" ) func TestConvertNamespacedToClusterRequestParameters(t *testing.T) { @@ -49,6 +53,7 @@ func TestConvertNamespacedToClusterRequestParameters(t *testing.T) { Mappings: []namespacedv1alpha2.Mapping{ { Method: "POST", + Action: "CREATE", URL: "http://example.com", Body: "test body", }, @@ -63,6 +68,7 @@ func TestConvertNamespacedToClusterRequestParameters(t *testing.T) { Mappings: []clusterv1alpha2.Mapping{ { Method: "POST", + Action: "CREATE", URL: "http://example.com", Body: "test body", }, @@ -125,6 +131,88 @@ func TestConvertNamespacedToClusterMappings(t *testing.T) { }, }, }, + { + name: "mapping with action field", + src: []namespacedv1alpha2.Mapping{ + { + Method: "POST", + Action: "CREATE", + URL: "http://api.example.com/users", + Body: `{"name": "test"}`, + Headers: map[string][]string{ + "Content-Type": {"application/json"}, + }, + }, + { + Action: "OBSERVE", + URL: "http://api.example.com/users/123", + }, + }, + want: []clusterv1alpha2.Mapping{ + { + Method: "POST", + Action: "CREATE", + URL: "http://api.example.com/users", + Body: `{"name": "test"}`, + Headers: map[string][]string{ + "Content-Type": {"application/json"}, + }, + }, + { + Action: "OBSERVE", + URL: "http://api.example.com/users/123", + }, + }, + }, + { + name: "multiple mappings with different actions", + src: []namespacedv1alpha2.Mapping{ + { + Action: "CREATE", + Method: "POST", + URL: "http://api.example.com/resources", + Body: `{"data": "create"}`, + }, + { + Action: "OBSERVE", + URL: "http://api.example.com/resources/{{.id}}", + }, + { + Action: "UPDATE", + Method: "PUT", + URL: "http://api.example.com/resources/{{.id}}", + Body: `{"data": "update"}`, + }, + { + Action: "REMOVE", + Method: "DELETE", + URL: "http://api.example.com/resources/{{.id}}", + }, + }, + want: []clusterv1alpha2.Mapping{ + { + Action: "CREATE", + Method: "POST", + URL: "http://api.example.com/resources", + Body: `{"data": "create"}`, + }, + { + Action: "OBSERVE", + URL: "http://api.example.com/resources/{{.id}}", + }, + { + Action: "UPDATE", + Method: "PUT", + URL: "http://api.example.com/resources/{{.id}}", + Body: `{"data": "update"}`, + }, + { + Action: "REMOVE", + Method: "DELETE", + URL: "http://api.example.com/resources/{{.id}}", + }, + }, + }, } for _, tt := range tests { @@ -321,6 +409,7 @@ func TestConvertNamespacedToClusterMappingStatus(t *testing.T) { name: "complete mapping", src: namespacedv1alpha2.Mapping{ Method: "PUT", + Action: "UPDATE", URL: "https://api.test.com/update", Body: `{"update": true}`, Headers: map[string][]string{ @@ -329,6 +418,7 @@ func TestConvertNamespacedToClusterMappingStatus(t *testing.T) { }, want: clusterv1alpha2.Mapping{ Method: "PUT", + Action: "UPDATE", URL: "https://api.test.com/update", Body: `{"update": true}`, Headers: map[string][]string{ @@ -336,6 +426,17 @@ func TestConvertNamespacedToClusterMappingStatus(t *testing.T) { }, }, }, + { + name: "mapping with action only", + src: namespacedv1alpha2.Mapping{ + Action: "OBSERVE", + URL: "https://api.test.com/resource/{{.id}}", + }, + want: clusterv1alpha2.Mapping{ + Action: "OBSERVE", + URL: "https://api.test.com/resource/{{.id}}", + }, + }, } for _, tt := range tests { @@ -689,3 +790,132 @@ func TestConvertNamespacedToClusterDisposableRequest(t *testing.T) { }) } } + +func TestConvertManagedResourceSpecToResourceSpec(t *testing.T) { + tests := []struct { + name string + src xpv2.ManagedResourceSpec + want xpv1.ResourceSpec + }{ + { + name: "empty spec", + src: xpv2.ManagedResourceSpec{}, + want: xpv1.ResourceSpec{}, + }, + { + name: "spec with basic fields", + src: xpv2.ManagedResourceSpec{ + WriteConnectionSecretToReference: &commonxp.LocalSecretReference{ + Name: "my-secret", + }, + ProviderConfigReference: &commonxp.ProviderConfigReference{ + Name: "my-provider-config", + Kind: "ProviderConfig", + }, + ManagementPolicies: []commonxp.ManagementAction{commonxp.ManagementActionAll}, + }, + want: xpv1.ResourceSpec{ + WriteConnectionSecretToReference: &xpv1.SecretReference{ + Name: "my-secret", + }, + ProviderConfigReference: &xpv1.Reference{ + Name: "my-provider-config", + }, + ManagementPolicies: []commonxp.ManagementAction{commonxp.ManagementActionAll}, + }, + }, + { + name: "spec with nil references", + src: xpv2.ManagedResourceSpec{ + ManagementPolicies: []commonxp.ManagementAction{commonxp.ManagementActionObserve, commonxp.ManagementActionCreate}, + }, + want: xpv1.ResourceSpec{ + ManagementPolicies: []commonxp.ManagementAction{commonxp.ManagementActionObserve, commonxp.ManagementActionCreate}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertManagedResourceSpecToResourceSpec(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertManagedResourceSpecToResourceSpec() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestConvertLocalSecretToSecretReference(t *testing.T) { + tests := []struct { + name string + src *commonxp.LocalSecretReference + want *xpv1.SecretReference + }{ + { + name: "nil input", + src: nil, + want: nil, + }, + { + name: "basic conversion", + src: &commonxp.LocalSecretReference{ + Name: "my-connection-secret", + }, + want: &xpv1.SecretReference{ + Name: "my-connection-secret", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertLocalSecretToSecretReference(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertLocalSecretToSecretReference() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestConvertProviderConfigReferenceToReference(t *testing.T) { + tests := []struct { + name string + src *commonxp.ProviderConfigReference + want *xpv1.Reference + }{ + { + name: "nil input", + src: nil, + want: nil, + }, + { + name: "basic conversion", + src: &commonxp.ProviderConfigReference{ + Name: "my-provider-config", + Kind: "ProviderConfig", + }, + want: &xpv1.Reference{ + Name: "my-provider-config", + }, + }, + { + name: "cluster provider config", + src: &commonxp.ProviderConfigReference{ + Name: "my-cluster-config", + Kind: "ClusterProviderConfig", + }, + want: &xpv1.Reference{ + Name: "my-cluster-config", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertProviderConfigReferenceToReference(tt.src) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertProviderConfigReferenceToReference() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/data-patcher/patch.go b/internal/data-patcher/patch.go index 3ccecbe..342086c 100644 --- a/internal/data-patcher/patch.go +++ b/internal/data-patcher/patch.go @@ -139,7 +139,13 @@ func ApplyResponseDataToSecrets(ctx context.Context, localKube client.Client, lo var owner metav1.Object = nil if ref.SetOwnerReference { - owner = cr + // Kubernetes disallows cross-namespace owner references + // Only set owner reference if the secret is in the same namespace as the owner + if cr.GetNamespace() == ref.SecretRef.Namespace { + owner = cr + } else { + logger.Debug(fmt.Sprintf("Skipping owner reference for cross-namespace secret injection: owner in %s, secret in %s", cr.GetNamespace(), ref.SecretRef.Namespace)) + } } // Use the cumulative response for patching (gets updated with secret placeholders) diff --git a/internal/data-patcher/patch_test.go b/internal/data-patcher/patch_test.go index 48f8f0d..9fd792f 100644 --- a/internal/data-patcher/patch_test.go +++ b/internal/data-patcher/patch_test.go @@ -4,11 +4,14 @@ import ( "context" "testing" + "github.com/crossplane-contrib/provider-http/apis/common" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -195,3 +198,144 @@ func TestPatchSecretsIntoHeaders(t *testing.T) { }) } } + +func TestApplyResponseDataToSecrets_CrossNamespaceOwnerReference(t *testing.T) { + type args struct { + ctx context.Context + localKube client.Client + logger logging.Logger + response *httpClient.HttpResponse + secretConfigs []common.SecretInjectionConfig + cr metav1.Object + } + + // Mock managed resource for testing + mockCR := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cr", + Namespace: "default", + }, + } + + response := &httpClient.HttpResponse{ + StatusCode: 200, + Body: `{"status": "success", "id": "123"}`, + Headers: map[string][]string{"Content-Type": {"application/json"}}, + } + + cases := map[string]struct { + args args + }{ + "SameNamespaceSecretInjectionWithOwnerReference": { + args: args{ + ctx: context.Background(), + logger: logging.NewNopLogger(), + localKube: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + return errors.New("secret not found") // Simulate secret doesn't exist + }, + MockUpdate: func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + return nil + }, + MockCreate: func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { + return nil + }, + }, + response: response, + secretConfigs: []common.SecretInjectionConfig{ + { + SecretRef: common.SecretRef{ + Name: "test-secret", + Namespace: "default", // Same namespace as CR + }, + SetOwnerReference: true, + KeyMappings: []common.KeyInjection{ + { + SecretKey: "result", + ResponseJQ: ".status", + }, + }, + }, + }, + cr: mockCR, + }, + }, + "CrossNamespaceSecretInjectionWithOwnerReferenceIgnored": { + args: args{ + ctx: context.Background(), + logger: logging.NewNopLogger(), + localKube: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + return errors.New("secret not found") // Simulate secret doesn't exist + }, + MockUpdate: func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + return nil + }, + MockCreate: func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { + return nil + }, + }, + response: response, + secretConfigs: []common.SecretInjectionConfig{ + { + SecretRef: common.SecretRef{ + Name: "test-secret", + Namespace: "crossplane-system", // Different namespace than CR + }, + SetOwnerReference: true, // This should be ignored + KeyMappings: []common.KeyInjection{ + { + SecretKey: "result", + ResponseJQ: ".status", + }, + }, + }, + }, + cr: mockCR, + }, + }, + "CrossNamespaceSecretInjectionWithoutOwnerReference": { + args: args{ + ctx: context.Background(), + logger: logging.NewNopLogger(), + localKube: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + return errors.New("secret not found") // Simulate secret doesn't exist + }, + MockUpdate: func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + return nil + }, + MockCreate: func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { + return nil + }, + }, + response: response, + secretConfigs: []common.SecretInjectionConfig{ + { + SecretRef: common.SecretRef{ + Name: "test-secret", + Namespace: "crossplane-system", // Different namespace than CR + }, + SetOwnerReference: false, // Explicitly disabled + KeyMappings: []common.KeyInjection{ + { + SecretKey: "result", + ResponseJQ: ".status", + }, + }, + }, + }, + cr: mockCR, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + // Test that the function doesn't panic and handles cross-namespace scenarios gracefully + ApplyResponseDataToSecrets(tc.args.ctx, tc.args.localKube, tc.args.logger, tc.args.response, tc.args.secretConfigs, tc.args.cr) + + // Test passes if no panic occurs - the cross-namespace logic should handle this gracefully + }) + } +} diff --git a/package/crds/http.m.crossplane.io_disposablerequests.yaml b/package/crds/http.m.crossplane.io_disposablerequests.yaml index a6e574d..7d69cf0 100644 --- a/package/crds/http.m.crossplane.io_disposablerequests.yaml +++ b/package/crds/http.m.crossplane.io_disposablerequests.yaml @@ -56,20 +56,6 @@ spec: spec: description: A DisposableRequestSpec defines the desired state of a DisposableRequest. properties: - deletionPolicy: - default: Delete - description: |- - DeletionPolicy specifies what will happen to the underlying external - when this managed resource is deleted - either "Delete" or "Orphan" the - external resource. - This field is planned to be deprecated in favor of the ManagementPolicies - field in a future release. Currently, both could be set independently and - non-default values would be honored if the feature flag is enabled. - See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 - enum: - - Orphan - - Delete - type: string forProvider: description: DisposableRequestParameters are the configurable fields of a DisposableRequest. @@ -229,10 +215,6 @@ spec: through a Crossplane feature flag. ManagementPolicies specify the array of actions Crossplane is allowed to take on the managed and external resources. - This field is planned to replace the DeletionPolicy field in a future - release. Currently, both could be set independently and non-default - values would be honored if the feature flag is enabled. If both are - custom, the DeletionPolicy field will be ignored. See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 and this one: https://github.com/crossplane/crossplane/blob/444267e84783136daa93568b364a5f01228cacbe/design/one-pager-ignore-changes.md items: @@ -250,41 +232,21 @@ spec: type: array providerConfigRef: default: + kind: ClusterProviderConfig name: default description: |- ProviderConfigReference specifies how the provider that will be used to create, observe, update, and delete this managed resource should be configured. properties: + kind: + description: Kind of the referenced object. + type: string name: description: Name of the referenced object. type: string - policy: - description: Policies for referencing. - properties: - resolution: - default: Required - description: |- - Resolution specifies whether resolution of this reference is required. - The default is 'Required', which means the reconcile will fail if the - reference cannot be resolved. 'Optional' means this reference will be - a no-op if it cannot be resolved. - enum: - - Required - - Optional - type: string - resolve: - description: |- - Resolve specifies when this reference should be resolved. The default - is 'IfNotPresent', which will attempt to resolve the reference only when - the corresponding field is not present. Use 'Always' to resolve the - reference on every reconcile. - enum: - - Always - - IfNotPresent - type: string - type: object required: + - kind - name type: object writeConnectionSecretToRef: @@ -297,12 +259,8 @@ spec: name: description: Name of the secret. type: string - namespace: - description: Namespace of the secret. - type: string required: - name - - namespace type: object required: - forProvider diff --git a/package/crds/http.m.crossplane.io_requests.yaml b/package/crds/http.m.crossplane.io_requests.yaml index 4a00634..028d4c7 100644 --- a/package/crds/http.m.crossplane.io_requests.yaml +++ b/package/crds/http.m.crossplane.io_requests.yaml @@ -56,20 +56,6 @@ spec: spec: description: A RequestSpec defines the desired state of a Request. properties: - deletionPolicy: - default: Delete - description: |- - DeletionPolicy specifies what will happen to the underlying external - when this managed resource is deleted - either "Delete" or "Orphan" the - external resource. - This field is planned to be deprecated in favor of the ManagementPolicies - field in a future release. Currently, both could be set independently and - non-default values would be honored if the feature flag is enabled. - See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 - enum: - - Orphan - - Delete - type: string forProvider: description: RequestParameters are the configurable fields of a Request. properties: @@ -279,10 +265,6 @@ spec: through a Crossplane feature flag. ManagementPolicies specify the array of actions Crossplane is allowed to take on the managed and external resources. - This field is planned to replace the DeletionPolicy field in a future - release. Currently, both could be set independently and non-default - values would be honored if the feature flag is enabled. If both are - custom, the DeletionPolicy field will be ignored. See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 and this one: https://github.com/crossplane/crossplane/blob/444267e84783136daa93568b364a5f01228cacbe/design/one-pager-ignore-changes.md items: @@ -300,41 +282,21 @@ spec: type: array providerConfigRef: default: + kind: ClusterProviderConfig name: default description: |- ProviderConfigReference specifies how the provider that will be used to create, observe, update, and delete this managed resource should be configured. properties: + kind: + description: Kind of the referenced object. + type: string name: description: Name of the referenced object. type: string - policy: - description: Policies for referencing. - properties: - resolution: - default: Required - description: |- - Resolution specifies whether resolution of this reference is required. - The default is 'Required', which means the reconcile will fail if the - reference cannot be resolved. 'Optional' means this reference will be - a no-op if it cannot be resolved. - enum: - - Required - - Optional - type: string - resolve: - description: |- - Resolve specifies when this reference should be resolved. The default - is 'IfNotPresent', which will attempt to resolve the reference only when - the corresponding field is not present. Use 'Always' to resolve the - reference on every reconcile. - enum: - - Always - - IfNotPresent - type: string - type: object required: + - kind - name type: object writeConnectionSecretToRef: @@ -347,12 +309,8 @@ spec: name: description: Name of the secret. type: string - namespace: - description: Namespace of the secret. - type: string required: - name - - namespace type: object required: - forProvider From c4f3a5c79667544a2e3e905f23dcc468b0d99bbf Mon Sep 17 00:00:00 2001 From: Riccardo Capraro Date: Thu, 23 Oct 2025 11:07:48 +0200 Subject: [PATCH 5/9] Fix deletion of namespaced disposable requests --- .../v1alpha2/providerconfig_types.go | 2 +- go.mod | 8 + go.sum | 8 + .../disposablerequest/disposablerequest.go | 6 +- .../disposablerequest_test.go | 675 +++++++++--------- 5 files changed, 358 insertions(+), 341 deletions(-) diff --git a/apis/namespaced/v1alpha2/providerconfig_types.go b/apis/namespaced/v1alpha2/providerconfig_types.go index 9754188..c849740 100644 --- a/apis/namespaced/v1alpha2/providerconfig_types.go +++ b/apis/namespaced/v1alpha2/providerconfig_types.go @@ -166,4 +166,4 @@ func (pcul *ProviderConfigUsageList) GetItems() []resource.ProviderConfigUsage { func init() { SchemeBuilder.Register(&ProviderConfig{}, &ProviderConfigList{}) SchemeBuilder.Register(&ProviderConfigUsage{}, &ProviderConfigUsageList{}) -} \ No newline at end of file +} diff --git a/go.mod b/go.mod index 4e90223..c39d713 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,10 @@ go 1.24.0 toolchain go1.24.6 +tool sigs.k8s.io/controller-tools/cmd/controller-gen + +tool github.com/crossplane/crossplane-tools/cmd/angryjet + require ( github.com/crossplane/crossplane-runtime/v2 v2.0.0 github.com/google/go-cmp v0.7.0 @@ -16,7 +20,10 @@ require ( require ( dario.cat/mergo v1.0.1 // indirect + github.com/alecthomas/kingpin/v2 v2.4.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect + github.com/crossplane/crossplane-tools v0.0.0-20251017183449-dd4517244339 // indirect + github.com/dave/jennifer v1.7.1 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/google/btree v1.1.3 // indirect @@ -24,6 +31,7 @@ require ( github.com/itchyny/timefmt-go v0.1.6 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.opentelemetry.io/otel v1.33.0 // indirect go.opentelemetry.io/otel/trace v1.33.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect diff --git a/go.sum b/go.sum index 3d610f5..34ec2f3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= @@ -14,6 +16,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/crossplane/crossplane-runtime/v2 v2.0.0 h1:PK2pTKfshdDZ5IfoiMRiCi0PBnIjqbS0KGXEJgRdrb4= github.com/crossplane/crossplane-runtime/v2 v2.0.0/go.mod h1:pkd5UzmE8esaZAApevMutR832GjJ1Qgc5Ngr78ByxrI= +github.com/crossplane/crossplane-tools v0.0.0-20251017183449-dd4517244339 h1:MPbMxSlY+82UsjrLUAGyXlh/iX1tL5WNj8W9SOaq/nk= +github.com/crossplane/crossplane-tools v0.0.0-20251017183449-dd4517244339/go.mod h1:8etxwmP4cZwJDwen4+PQlnc1tggltAhEfyyigmdHulQ= +github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= +github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -145,6 +151,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= diff --git a/internal/controller/namespaced/disposablerequest/disposablerequest.go b/internal/controller/namespaced/disposablerequest/disposablerequest.go index 76cfffe..4200943 100644 --- a/internal/controller/namespaced/disposablerequest/disposablerequest.go +++ b/internal/controller/namespaced/disposablerequest/disposablerequest.go @@ -25,6 +25,7 @@ import ( datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" "github.com/crossplane-contrib/provider-http/internal/jq" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -164,14 +165,15 @@ type external struct { } // Observe checks the state of the DisposableRequest resource and updates its status accordingly. +// +//gocyclo:ignore func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { cr, ok := mg.(*v1alpha2.DisposableRequest) if !ok { return managed.ExternalObservation{}, errors.New(errNotNamespacedDisposableRequest) } - // Skip secret injection processing if the resource is being deleted - if cr.GetDeletionTimestamp() != nil { + if meta.WasDeleted(mg) { c.logger.Debug("DisposableRequest is being deleted, skipping observation and secret injection") return managed.ExternalObservation{ ResourceExists: false, diff --git a/internal/controller/namespaced/disposablerequest/disposablerequest_test.go b/internal/controller/namespaced/disposablerequest/disposablerequest_test.go index 73a0340..5d16721 100644 --- a/internal/controller/namespaced/disposablerequest/disposablerequest_test.go +++ b/internal/controller/namespaced/disposablerequest/disposablerequest_test.go @@ -1,338 +1,337 @@ -/* -Copyright 2023 The Crossplane Authors. - -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 disposablerequest - -import ( - "context" - "testing" - - "github.com/crossplane-contrib/provider-http/apis/namespaced/disposablerequest/v1alpha2" - httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" - "github.com/crossplane/crossplane-runtime/v2/pkg/logging" - "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" - "github.com/crossplane/crossplane-runtime/v2/pkg/resource" - "github.com/crossplane/crossplane-runtime/v2/pkg/test" - "github.com/google/go-cmp/cmp" - "github.com/pkg/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -var ( - errBoom = errors.New("boom") -) - -type MockHttpClient struct { - MockSendRequest func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) -} - -func (m *MockHttpClient) SendRequest(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { - return m.MockSendRequest(ctx, method, url, body, headers, skipTLSVerify) -} - -type notNamespacedDisposableRequest struct { - resource.Managed -} - -func namespacedDisposableRequest(modifiers ...func(*v1alpha2.DisposableRequest)) *v1alpha2.DisposableRequest { - cr := &v1alpha2.DisposableRequest{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-disposable", - Namespace: "default", - }, - Spec: v1alpha2.DisposableRequestSpec{ - ForProvider: v1alpha2.DisposableRequestParameters{ - URL: "http://example.com/test", - Method: "POST", - Body: `{"test": true}`, - }, - }, - } - - for _, modifier := range modifiers { - modifier(cr) - } - - return cr -} - -func namespacedDisposableRequestWithDeletion() *v1alpha2.DisposableRequest { - now := metav1.Now() - return namespacedDisposableRequest(func(cr *v1alpha2.DisposableRequest) { - cr.DeletionTimestamp = &now - }) -} - - -func TestObserve(t *testing.T) { - type args struct { - http httpClient.Client - localKube client.Client - mg resource.Managed - } - type want struct { - obs managed.ExternalObservation - err error - } - - cases := []struct { - name string - args args - want want - }{ - { - name: "NotNamespacedDisposableRequest", - args: args{ - mg: notNamespacedDisposableRequest{}, - }, - want: want{ - err: errors.New(errNotNamespacedDisposableRequest), - }, - }, - { - name: "ResourceBeingDeleted", - args: args{ - mg: namespacedDisposableRequestWithDeletion(), - }, - want: want{ - obs: managed.ExternalObservation{ - ResourceExists: false, - }, - }, - }, - { - name: "ResourceNotSynced", - args: args{ - mg: namespacedDisposableRequest(), - }, - want: want{ - obs: managed.ExternalObservation{ - ResourceExists: false, - }, - }, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - e := &external{ - logger: logging.NewNopLogger(), - localKube: tc.args.localKube, - http: tc.args.http, - } - - got, err := e.Observe(context.Background(), tc.args.mg) - if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { - t.Errorf("Observe(...): -want error, +got error: %s", diff) - } - if diff := cmp.Diff(tc.want.obs, got); diff != "" { - t.Errorf("Observe(...): -want, +got: %s", diff) - } - }) - } -} - -func TestCreate(t *testing.T) { - type args struct { - http httpClient.Client - localKube client.Client - mg resource.Managed - } - type want struct { - err error - } - - cases := []struct { - name string - args args - want want - }{ - { - name: "NotNamespacedDisposableRequest", - args: args{ - mg: notNamespacedDisposableRequest{}, - }, - want: want{ - err: errors.New(errNotNamespacedDisposableRequest), - }, - }, - { - name: "HttpRequestFailed", - args: args{ - http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { - return httpClient.HttpDetails{}, errBoom - }, - }, - localKube: &test.MockClient{ - MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), - MockGet: test.NewMockGetFn(nil), - }, - mg: namespacedDisposableRequest(), - }, - want: want{ - err: errors.Wrap(errBoom, errFailedToSendHttpDisposableRequest), - }, - }, - { - name: "Success", - args: args{ - http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { - return httpClient.HttpDetails{ - HttpResponse: httpClient.HttpResponse{ - StatusCode: 200, - Body: `{"result": "success"}`, - }, - }, nil - }, - }, - localKube: &test.MockClient{ - MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), - MockGet: test.NewMockGetFn(nil), - }, - mg: namespacedDisposableRequest(), - }, - want: want{ - err: nil, - }, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - e := &external{ - logger: logging.NewNopLogger(), - localKube: tc.args.localKube, - http: tc.args.http, - } - - _, err := e.Create(context.Background(), tc.args.mg) - if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { - t.Errorf("Create(...): -want error, +got error: %s", diff) - } - }) - } -} - -func TestUpdate(t *testing.T) { - type args struct { - http httpClient.Client - localKube client.Client - mg resource.Managed - } - type want struct { - err error - } - - cases := []struct { - name string - args args - want want - }{ - { - name: "NotNamespacedDisposableRequest", - args: args{ - mg: notNamespacedDisposableRequest{}, - }, - want: want{ - err: errors.New(errNotNamespacedDisposableRequest), - }, - }, - { - name: "Success", - args: args{ - http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { - return httpClient.HttpDetails{ - HttpResponse: httpClient.HttpResponse{ - StatusCode: 200, - Body: `{"result": "updated"}`, - }, - }, nil - }, - }, - localKube: &test.MockClient{ - MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), - MockGet: test.NewMockGetFn(nil), - }, - mg: namespacedDisposableRequest(), - }, - want: want{ - err: nil, - }, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - e := &external{ - logger: logging.NewNopLogger(), - localKube: tc.args.localKube, - http: tc.args.http, - } - - _, err := e.Update(context.Background(), tc.args.mg) - if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { - t.Errorf("Update(...): -want error, +got error: %s", diff) - } - }) - } -} - -func TestDelete(t *testing.T) { - type args struct { - mg resource.Managed - } - type want struct { - result managed.ExternalDelete - err error - } - - cases := []struct { - name string - args args - want want - }{ - { - name: "Success", - args: args{ - mg: namespacedDisposableRequest(), - }, - want: want{ - result: managed.ExternalDelete{}, - err: nil, - }, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - e := &external{ - logger: logging.NewNopLogger(), - } - - got, err := e.Delete(context.Background(), tc.args.mg) - if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { - t.Errorf("Delete(...): -want error, +got error: %s", diff) - } - if diff := cmp.Diff(tc.want.result, got); diff != "" { - t.Errorf("Delete(...): -want result, +got result: %s", diff) - } - }) - } -} \ No newline at end of file +/* +Copyright 2023 The Crossplane Authors. + +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 disposablerequest + +import ( + "context" + "testing" + + "github.com/crossplane-contrib/provider-http/apis/namespaced/disposablerequest/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + errBoom = errors.New("boom") +) + +type MockHttpClient struct { + MockSendRequest func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) +} + +func (m *MockHttpClient) SendRequest(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + return m.MockSendRequest(ctx, method, url, body, headers, skipTLSVerify) +} + +type notNamespacedDisposableRequest struct { + resource.Managed +} + +func namespacedDisposableRequest(modifiers ...func(*v1alpha2.DisposableRequest)) *v1alpha2.DisposableRequest { + cr := &v1alpha2.DisposableRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disposable", + Namespace: "default", + }, + Spec: v1alpha2.DisposableRequestSpec{ + ForProvider: v1alpha2.DisposableRequestParameters{ + URL: "http://example.com/test", + Method: "POST", + Body: `{"test": true}`, + }, + }, + } + + for _, modifier := range modifiers { + modifier(cr) + } + + return cr +} + +func namespacedDisposableRequestWithDeletion() *v1alpha2.DisposableRequest { + now := metav1.Now() + return namespacedDisposableRequest(func(cr *v1alpha2.DisposableRequest) { + cr.DeletionTimestamp = &now + }) +} + +func TestObserve(t *testing.T) { + type args struct { + http httpClient.Client + localKube client.Client + mg resource.Managed + } + type want struct { + obs managed.ExternalObservation + err error + } + + cases := []struct { + name string + args args + want want + }{ + { + name: "NotNamespacedDisposableRequest", + args: args{ + mg: notNamespacedDisposableRequest{}, + }, + want: want{ + err: errors.New(errNotNamespacedDisposableRequest), + }, + }, + { + name: "ResourceBeingDeleted", + args: args{ + mg: namespacedDisposableRequestWithDeletion(), + }, + want: want{ + obs: managed.ExternalObservation{ + ResourceExists: false, + }, + }, + }, + { + name: "ResourceNotSynced", + args: args{ + mg: namespacedDisposableRequest(), + }, + want: want{ + obs: managed.ExternalObservation{ + ResourceExists: false, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + e := &external{ + logger: logging.NewNopLogger(), + localKube: tc.args.localKube, + http: tc.args.http, + } + + got, err := e.Observe(context.Background(), tc.args.mg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("Observe(...): -want error, +got error: %s", diff) + } + if diff := cmp.Diff(tc.want.obs, got); diff != "" { + t.Errorf("Observe(...): -want, +got: %s", diff) + } + }) + } +} + +func TestCreate(t *testing.T) { + type args struct { + http httpClient.Client + localKube client.Client + mg resource.Managed + } + type want struct { + err error + } + + cases := []struct { + name string + args args + want want + }{ + { + name: "NotNamespacedDisposableRequest", + args: args{ + mg: notNamespacedDisposableRequest{}, + }, + want: want{ + err: errors.New(errNotNamespacedDisposableRequest), + }, + }, + { + name: "HttpRequestFailed", + args: args{ + http: &MockHttpClient{ + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + return httpClient.HttpDetails{}, errBoom + }, + }, + localKube: &test.MockClient{ + MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), + MockGet: test.NewMockGetFn(nil), + }, + mg: namespacedDisposableRequest(), + }, + want: want{ + err: errors.Wrap(errBoom, errFailedToSendHttpDisposableRequest), + }, + }, + { + name: "Success", + args: args{ + http: &MockHttpClient{ + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + return httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + StatusCode: 200, + Body: `{"result": "success"}`, + }, + }, nil + }, + }, + localKube: &test.MockClient{ + MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), + MockGet: test.NewMockGetFn(nil), + }, + mg: namespacedDisposableRequest(), + }, + want: want{ + err: nil, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + e := &external{ + logger: logging.NewNopLogger(), + localKube: tc.args.localKube, + http: tc.args.http, + } + + _, err := e.Create(context.Background(), tc.args.mg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("Create(...): -want error, +got error: %s", diff) + } + }) + } +} + +func TestUpdate(t *testing.T) { + type args struct { + http httpClient.Client + localKube client.Client + mg resource.Managed + } + type want struct { + err error + } + + cases := []struct { + name string + args args + want want + }{ + { + name: "NotNamespacedDisposableRequest", + args: args{ + mg: notNamespacedDisposableRequest{}, + }, + want: want{ + err: errors.New(errNotNamespacedDisposableRequest), + }, + }, + { + name: "Success", + args: args{ + http: &MockHttpClient{ + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + return httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + StatusCode: 200, + Body: `{"result": "updated"}`, + }, + }, nil + }, + }, + localKube: &test.MockClient{ + MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), + MockGet: test.NewMockGetFn(nil), + }, + mg: namespacedDisposableRequest(), + }, + want: want{ + err: nil, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + e := &external{ + logger: logging.NewNopLogger(), + localKube: tc.args.localKube, + http: tc.args.http, + } + + _, err := e.Update(context.Background(), tc.args.mg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("Update(...): -want error, +got error: %s", diff) + } + }) + } +} + +func TestDelete(t *testing.T) { + type args struct { + mg resource.Managed + } + type want struct { + result managed.ExternalDelete + err error + } + + cases := []struct { + name string + args args + want want + }{ + { + name: "Success", + args: args{ + mg: namespacedDisposableRequest(), + }, + want: want{ + result: managed.ExternalDelete{}, + err: nil, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + e := &external{ + logger: logging.NewNopLogger(), + } + + got, err := e.Delete(context.Background(), tc.args.mg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("Delete(...): -want error, +got error: %s", diff) + } + if diff := cmp.Diff(tc.want.result, got); diff != "" { + t.Errorf("Delete(...): -want result, +got result: %s", diff) + } + }) + } +} From 45d6b986c0c1ab87845a17bd31bff4d55913faaa Mon Sep 17 00:00:00 2001 From: Riccardo Capraro Date: Thu, 23 Oct 2025 13:07:04 +0200 Subject: [PATCH 6/9] Skip secret injection during deletion of resources --- .../disposablerequest/disposablerequest.go | 16 ++ .../disposablerequest_test.go | 127 +++++++++++ .../controller/cluster/request/request.go | 16 +- .../cluster/request/request_test.go | 114 ++++++++++ .../controller/namespaced/request/request.go | 16 +- .../namespaced/request/request_test.go | 207 ++++++++++++++++++ 6 files changed, 494 insertions(+), 2 deletions(-) create mode 100644 internal/controller/namespaced/request/request_test.go diff --git a/internal/controller/cluster/disposablerequest/disposablerequest.go b/internal/controller/cluster/disposablerequest/disposablerequest.go index 8ffe012..e3e2cc9 100644 --- a/internal/controller/cluster/disposablerequest/disposablerequest.go +++ b/internal/controller/cluster/disposablerequest/disposablerequest.go @@ -25,6 +25,7 @@ import ( datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" "github.com/crossplane-contrib/provider-http/internal/jq" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -147,12 +148,21 @@ type external struct { } // Observe checks the state of the DisposableRequest resource and updates its status accordingly. +// +//gocyclo:ignore func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { cr, ok := mg.(*v1alpha2.DisposableRequest) if !ok { return managed.ExternalObservation{}, errors.New(errNotDisposableRequest) } + if meta.WasDeleted(mg) { + c.logger.Debug("DisposableRequest is being deleted, skipping observation and secret injection") + return managed.ExternalObservation{ + ResourceExists: false, + }, nil + } + isUpToDate := !(utils.ShouldRetry(cr.Spec.ForProvider.RollbackRetriesLimit, cr.Status.Failed) && !utils.RetriesLimitReached(cr.Status.Failed, cr.Spec.ForProvider.RollbackRetriesLimit)) isAvailable := isUpToDate @@ -270,6 +280,12 @@ func (c *external) deployAction(ctx context.Context, cr *v1alpha2.DisposableRequ // applySecretInjectionsFromStoredResponse applies secret injection configurations using the stored response // This is used when the resource is already synced but secret injection configs may have been updated func (c *external) applySecretInjectionsFromStoredResponse(ctx context.Context, cr *v1alpha2.DisposableRequest, storedResponse httpClient.HttpResponse, isExpectedResponse bool) { + // Skip secret injection during deletion to avoid cross-namespace owner reference issues + if meta.WasDeleted(cr) { + c.logger.Debug("DisposableRequest is being deleted, skipping secret injection") + return + } + if isExpectedResponse { c.logger.Debug("Applying secret injections from stored response") datapatcher.ApplyResponseDataToSecrets(ctx, c.localKube, c.logger, &storedResponse, cr.Spec.ForProvider.SecretInjectionConfigs, cr) diff --git a/internal/controller/cluster/disposablerequest/disposablerequest_test.go b/internal/controller/cluster/disposablerequest/disposablerequest_test.go index 5f08df4..cb54ffe 100644 --- a/internal/controller/cluster/disposablerequest/disposablerequest_test.go +++ b/internal/controller/cluster/disposablerequest/disposablerequest_test.go @@ -32,6 +32,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) @@ -433,3 +434,129 @@ func Test_deployAction(t *testing.T) { }) } } + +func disposableRequestWithDeletion() *v1alpha2.DisposableRequest { + now := v1.Now() + return &v1alpha2.DisposableRequest{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-disposable", + Namespace: "default", + DeletionTimestamp: &now, + }, + Spec: v1alpha2.DisposableRequestSpec{ + ForProvider: v1alpha2.DisposableRequestParameters{ + URL: "http://example.com/test", + Method: "POST", + Body: `{"test": true}`, + }, + }, + } +} + +func TestObserve_DeletionMonitoring(t *testing.T) { + type args struct { + http httpClient.Client + localKube client.Client + mg resource.Managed + } + type want struct { + obs managed.ExternalObservation + err error + } + + cases := []struct { + name string + args args + want want + }{ + { + name: "ResourceBeingDeleted", + args: args{ + mg: disposableRequestWithDeletion(), + }, + want: want{ + obs: managed.ExternalObservation{ + ResourceExists: false, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + e := &external{ + logger: logging.NewNopLogger(), + localKube: tc.args.localKube, + http: tc.args.http, + } + + got, err := e.Observe(context.Background(), tc.args.mg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("Observe(...): -want error, +got error: %s", diff) + } + if diff := cmp.Diff(tc.want.obs, got); diff != "" { + t.Errorf("Observe(...): -want, +got: %s", diff) + } + }) + } +} + +func TestApplySecretInjectionsFromStoredResponse_SkipDuringDeletion(t *testing.T) { + type args struct { + cr *v1alpha2.DisposableRequest + storedResponse httpClient.HttpResponse + isExpectedResponse bool + } + + cases := []struct { + name string + args args + }{ + { + name: "SkipSecretInjectionForDeletingResource", + args: args{ + cr: disposableRequestWithDeletion(), + storedResponse: httpClient.HttpResponse{ + StatusCode: 200, + Body: `{"result": "success"}`, + }, + isExpectedResponse: true, + }, + }, + { + name: "NormalSecretInjectionForNonDeletingResource", + args: args{ + cr: &v1alpha2.DisposableRequest{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-disposable", + Namespace: "default", + }, + Spec: v1alpha2.DisposableRequestSpec{ + ForProvider: v1alpha2.DisposableRequestParameters{ + URL: "http://example.com/test", + Method: "POST", + Body: `{"test": true}`, + }, + }, + }, + storedResponse: httpClient.HttpResponse{ + StatusCode: 200, + Body: `{"result": "success"}`, + }, + isExpectedResponse: true, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + e := &external{ + logger: logging.NewNopLogger(), + localKube: &test.MockClient{}, + } + + // This test verifies that the method doesn't panic and handles deletion gracefully + e.applySecretInjectionsFromStoredResponse(context.Background(), tc.args.cr, tc.args.storedResponse, tc.args.isExpectedResponse) + }) + } +} diff --git a/internal/controller/cluster/request/request.go b/internal/controller/cluster/request/request.go index 884cfe6..6c4fa5d 100644 --- a/internal/controller/cluster/request/request.go +++ b/internal/controller/cluster/request/request.go @@ -21,6 +21,7 @@ import ( "time" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -149,6 +150,13 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex return managed.ExternalObservation{}, errors.New(errNotRequest) } + if meta.WasDeleted(mg) { + c.logger.Debug("Request is being deleted, skipping observation and secret injection") + return managed.ExternalObservation{ + ResourceExists: false, + }, nil + } + observeRequestDetails, err := c.isUpToDate(ctx, cr) if err != nil && err.Error() == observe.ErrObjectNotFound { return managed.ExternalObservation{ @@ -202,7 +210,13 @@ func (c *external) deployAction(ctx context.Context, cr *v1alpha2.Request, actio } details, err := c.http.SendRequest(ctx, mapping.Method, requestDetails.Url, requestDetails.Body, requestDetails.Headers, cr.Spec.ForProvider.InsecureSkipTLSVerify) - datapatcher.ApplyResponseDataToSecrets(ctx, c.localKube, c.logger, &details.HttpResponse, cr.Spec.ForProvider.SecretInjectionConfigs, cr) + + // Skip secret injection during deletion to avoid cross-namespace owner reference issues + if !meta.WasDeleted(cr) { + datapatcher.ApplyResponseDataToSecrets(ctx, c.localKube, c.logger, &details.HttpResponse, cr.Spec.ForProvider.SecretInjectionConfigs, cr) + } else { + c.logger.Debug("Request is being deleted, skipping secret injection") + } statusHandler, err := statushandler.NewStatusHandler(ctx, cr, details, err, c.localKube, c.logger) if err != nil { diff --git a/internal/controller/cluster/request/request_test.go b/internal/controller/cluster/request/request_test.go index bf84333..aa20e74 100644 --- a/internal/controller/cluster/request/request_test.go +++ b/internal/controller/cluster/request/request_test.go @@ -11,6 +11,7 @@ import ( httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/google/go-cmp/cmp" @@ -337,3 +338,116 @@ func Test_httpExternal_Delete(t *testing.T) { }) } } + +func httpRequestWithDeletion() *v1alpha2.Request { + now := v1.Now() + return httpRequest(func(r *v1alpha2.Request) { + r.DeletionTimestamp = &now + }) +} + +func TestObserve_DeletionMonitoring(t *testing.T) { + type args struct { + http httpClient.Client + localKube client.Client + mg resource.Managed + } + type want struct { + obs managed.ExternalObservation + err error + } + + cases := []struct { + name string + args args + want want + }{ + { + name: "ResourceBeingDeleted", + args: args{ + mg: httpRequestWithDeletion(), + }, + want: want{ + obs: managed.ExternalObservation{ + ResourceExists: false, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + e := &external{ + logger: logging.NewNopLogger(), + localKube: tc.args.localKube, + http: tc.args.http, + } + + got, err := e.Observe(context.Background(), tc.args.mg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("Observe(...): -want error, +got error: %s", diff) + } + if diff := cmp.Diff(tc.want.obs, got); diff != "" { + t.Errorf("Observe(...): -want, +got: %s", diff) + } + }) + } +} + +func TestDeployAction_SkipSecretInjectionDuringDeletion(t *testing.T) { + type args struct { + http httpClient.Client + localKube client.Client + cr *v1alpha2.Request + action string + } + type want struct { + err error + } + + cases := []struct { + name string + args args + want want + }{ + { + name: "SkipSecretInjectionForDeletingResource", + args: args{ + http: &MockHttpClient{ + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + return httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + StatusCode: 200, + Body: `{"result": "success"}`, + }, + }, nil + }, + }, + localKube: &test.MockClient{ + MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), + MockGet: test.NewMockGetFn(nil), + }, + cr: httpRequestWithDeletion(), + action: v1alpha2.ActionRemove, + }, + want: want{ + err: nil, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + e := &external{ + logger: logging.NewNopLogger(), + localKube: tc.args.localKube, + http: tc.args.http, + } + + err := e.deployAction(context.Background(), tc.args.cr, tc.args.action) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("deployAction(...): -want error, +got error: %s", diff) + } + }) + } +} diff --git a/internal/controller/namespaced/request/request.go b/internal/controller/namespaced/request/request.go index cc7d03f..b2fb8a5 100644 --- a/internal/controller/namespaced/request/request.go +++ b/internal/controller/namespaced/request/request.go @@ -21,6 +21,7 @@ import ( "time" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -162,6 +163,13 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex return managed.ExternalObservation{}, errors.New(errNotRequest) } + if meta.WasDeleted(mg) { + c.logger.Debug("Request is being deleted, skipping observation and secret injection") + return managed.ExternalObservation{ + ResourceExists: false, + }, nil + } + observeRequestDetails, err := c.isUpToDate(ctx, cr) if err != nil && err.Error() == observe.ErrObjectNotFound { return managed.ExternalObservation{ @@ -253,7 +261,13 @@ func (c *external) deployAction(ctx context.Context, cr *v1alpha2.Request, actio } details, err := c.http.SendRequest(ctx, mapping.Method, requestDetails.Url, requestDetails.Body, requestDetails.Headers, cr.Spec.ForProvider.InsecureSkipTLSVerify) - datapatcher.ApplyResponseDataToSecrets(ctx, c.localKube, c.logger, &details.HttpResponse, cr.Spec.ForProvider.SecretInjectionConfigs, cr) + + // Skip secret injection during deletion to avoid cross-namespace owner reference issues + if !meta.WasDeleted(cr) { + datapatcher.ApplyResponseDataToSecrets(ctx, c.localKube, c.logger, &details.HttpResponse, cr.Spec.ForProvider.SecretInjectionConfigs, cr) + } else { + c.logger.Debug("Request is being deleted, skipping secret injection") + } statusHandler, err := statushandler.NewStatusHandler(ctx, cr, details, err, c.localKube, c.logger) if err != nil { diff --git a/internal/controller/namespaced/request/request_test.go b/internal/controller/namespaced/request/request_test.go new file mode 100644 index 0000000..625d349 --- /dev/null +++ b/internal/controller/namespaced/request/request_test.go @@ -0,0 +1,207 @@ +package request + +import ( + "context" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/crossplane-contrib/provider-http/apis/namespaced/request/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" +) + +type MockHttpClient struct { + MockSendRequest func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) +} + +func (m *MockHttpClient) SendRequest(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + return m.MockSendRequest(ctx, method, url, body, headers, skipTLSVerify) +} + +type notNamespacedRequest struct { + resource.Managed +} + +func namespacedRequest(modifiers ...func(*v1alpha2.Request)) *v1alpha2.Request { + cr := &v1alpha2.Request{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-request", + Namespace: "default", + }, + Spec: v1alpha2.RequestSpec{ + ForProvider: v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: `{"test": true}`, + BaseUrl: "http://example.com/test", + }, + Mappings: []v1alpha2.Mapping{ + { + Method: "POST", + Action: "CREATE", + URL: ".payload.baseUrl", + Body: ".payload.body", + }, + }, + }, + }, + } + + for _, modifier := range modifiers { + modifier(cr) + } + + return cr +} + +func namespacedRequestWithDeletion() *v1alpha2.Request { + now := metav1.Now() + return namespacedRequest(func(cr *v1alpha2.Request) { + cr.DeletionTimestamp = &now + }) +} + +func TestObserve(t *testing.T) { + type args struct { + http httpClient.Client + localKube client.Client + mg resource.Managed + } + type want struct { + obs managed.ExternalObservation + err error + } + + cases := []struct { + name string + args args + want want + }{ + { + name: "NotNamespacedRequest", + args: args{ + mg: notNamespacedRequest{}, + }, + want: want{ + err: errors.New(errNotRequest), + }, + }, + { + name: "ResourceBeingDeleted", + args: args{ + mg: namespacedRequestWithDeletion(), + }, + want: want{ + obs: managed.ExternalObservation{ + ResourceExists: false, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + e := &external{ + logger: logging.NewNopLogger(), + localKube: tc.args.localKube, + http: tc.args.http, + } + + got, err := e.Observe(context.Background(), tc.args.mg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("Observe(...): -want error, +got error: %s", diff) + } + if diff := cmp.Diff(tc.want.obs, got); diff != "" { + t.Errorf("Observe(...): -want, +got: %s", diff) + } + }) + } +} + +func TestDeployAction_SkipSecretInjectionDuringDeletion(t *testing.T) { + type args struct { + http httpClient.Client + localKube client.Client + cr *v1alpha2.Request + action string + } + type want struct { + err error + } + + cases := []struct { + name string + args args + want want + }{ + { + name: "SkipSecretInjectionForDeletingResource", + args: args{ + http: &MockHttpClient{ + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + return httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + StatusCode: 200, + Body: `{"result": "success"}`, + }, + }, nil + }, + }, + localKube: &test.MockClient{ + MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), + MockGet: test.NewMockGetFn(nil), + }, + cr: namespacedRequestWithDeletion(), + action: v1alpha2.ActionRemove, + }, + want: want{ + err: nil, + }, + }, + { + name: "NormalSecretInjectionForNonDeletingResource", + args: args{ + http: &MockHttpClient{ + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + return httpClient.HttpDetails{ + HttpResponse: httpClient.HttpResponse{ + StatusCode: 200, + Body: `{"result": "success"}`, + }, + }, nil + }, + }, + localKube: &test.MockClient{ + MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), + MockGet: test.NewMockGetFn(nil), + }, + cr: namespacedRequest(), + action: v1alpha2.ActionCreate, + }, + want: want{ + err: nil, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + e := &external{ + logger: logging.NewNopLogger(), + localKube: tc.args.localKube, + http: tc.args.http, + } + + err := e.deployAction(context.Background(), tc.args.cr, tc.args.action) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("deployAction(...): -want error, +got error: %s", diff) + } + }) + } +} From 7661d6d42620569dbc135b56b7cc985843f4125c Mon Sep 17 00:00:00 2001 From: Riccardo Capraro Date: Fri, 14 Nov 2025 14:06:47 +0100 Subject: [PATCH 7/9] Publish provider image to Github --- .github/workflows/ci.yml | 66 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88ced83..c718957 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,9 +10,9 @@ on: env: # Common versions - GO_VERSION: '1.24.6' - GOLANGCI_VERSION: 'v2.1.2' - DOCKER_BUILDX_VERSION: 'v0.23.0' + GO_VERSION: "1.24.6" + GOLANGCI_VERSION: "v2.1.2" + DOCKER_BUILDX_VERSION: "v0.23.0" jobs: detect-noop: @@ -28,7 +28,6 @@ jobs: paths_ignore: '["**.md", "**.png", "**.jpg"]' do_not_skip: '["workflow_dispatch", "schedule", "push"]' - lint: runs-on: ubuntu-latest needs: detect-noop @@ -118,7 +117,7 @@ jobs: - name: "crossplane-v2" version: "2.0.2" cli-version: "v2.0.2" - - name: "crossplane-v1" + - name: "crossplane-v1" version: "1.20.1" cli-version: "v1.20.1" @@ -161,3 +160,60 @@ jobs: # Set Crossplane version for this matrix run CROSSPLANE_VERSION: ${{ matrix.crossplane-version.version }} CROSSPLANE_CLI_VERSION: ${{ matrix.crossplane-version.cli-version }} + + publish-artifacts: + runs-on: ubuntu-latest + needs: detect-noop + if: needs.detect-noop.outputs.noop != 'true' + + steps: + - name: Cleanup Disk + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 + with: + android: true + dotnet: true + haskell: true + tool-cache: true + large-packages: false + swap-storage: false + + - name: Setup QEMU + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 + with: + platforms: all + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 + with: + version: ${{ env.DOCKER_BUILDX_VERSION }} + install: true + + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + submodules: true + + - name: Fetch History + run: git fetch --prune --unshallow + + - name: Setup Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Vendor Dependencies + run: make vendor vendor.check + + - name: Build Artifacts + run: make -j2 build.all + env: + # We're using docker buildx, which doesn't actually load the images it + # builds by default. Specifying --load does so. + BUILD_ARGS: "--load" + + - name: Publish Artifacts to GitHub + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: output + path: _output/** + include-hidden-files: true From bff7283e6761294ad95c0d973f6364ecd11d6037 Mon Sep 17 00:00:00 2001 From: Riccardo Capraro Date: Fri, 14 Nov 2025 14:25:07 +0100 Subject: [PATCH 8/9] Test publishing to github packages --- .github/workflows/ci.yml | 57 ------------------- .../workflows/publish-provider-package.yaml | 11 ++-- 2 files changed, 5 insertions(+), 63 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c718957..db7ed12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,60 +160,3 @@ jobs: # Set Crossplane version for this matrix run CROSSPLANE_VERSION: ${{ matrix.crossplane-version.version }} CROSSPLANE_CLI_VERSION: ${{ matrix.crossplane-version.cli-version }} - - publish-artifacts: - runs-on: ubuntu-latest - needs: detect-noop - if: needs.detect-noop.outputs.noop != 'true' - - steps: - - name: Cleanup Disk - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 - with: - android: true - dotnet: true - haskell: true - tool-cache: true - large-packages: false - swap-storage: false - - - name: Setup QEMU - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 - with: - platforms: all - - - name: Setup Docker Buildx - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 - with: - version: ${{ env.DOCKER_BUILDX_VERSION }} - install: true - - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - submodules: true - - - name: Fetch History - run: git fetch --prune --unshallow - - - name: Setup Go - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Vendor Dependencies - run: make vendor vendor.check - - - name: Build Artifacts - run: make -j2 build.all - env: - # We're using docker buildx, which doesn't actually load the images it - # builds by default. Specifying --load does so. - BUILD_ARGS: "--load" - - - name: Publish Artifacts to GitHub - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 - with: - name: output - path: _output/** - include-hidden-files: true diff --git a/.github/workflows/publish-provider-package.yaml b/.github/workflows/publish-provider-package.yaml index 18f6d6e..03caf54 100644 --- a/.github/workflows/publish-provider-package.yaml +++ b/.github/workflows/publish-provider-package.yaml @@ -5,22 +5,21 @@ on: inputs: version: description: "Version string to use while publishing the package (e.g. v1.0.0-alpha.1)" - default: '' + default: "" required: false go-version: - description: 'Go version to use if building needs to be done' - default: '1.24' + description: "Go version to use if building needs to be done" + default: "1.24" required: false jobs: publish-provider-package: uses: crossplane-contrib/provider-workflows/.github/workflows/publish-provider-non-family.yml@main with: - repository: provider-http + repository: riccap version: ${{ github.event.inputs.version }} go-version: ${{ github.event.inputs.go-version }} cleanup-disk: true + mirror: false secrets: GHCR_PAT: ${{ secrets.GITHUB_TOKEN }} - XPKG_MIRROR_TOKEN: ${{ secrets.XPKG_TOKEN }} - XPKG_MIRROR_ACCESS_ID: ${{ secrets.XPKG_ACCESS_ID }} From 80aa69c4bfb3b32a0d240659a19a912ddf0e0a88 Mon Sep 17 00:00:00 2001 From: Riccardo Capraro Date: Fri, 14 Nov 2025 14:41:30 +0100 Subject: [PATCH 9/9] Test publishing to github packages --- .github/workflows/publish-provider-package.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-provider-package.yaml b/.github/workflows/publish-provider-package.yaml index 03caf54..4917dde 100644 --- a/.github/workflows/publish-provider-package.yaml +++ b/.github/workflows/publish-provider-package.yaml @@ -16,7 +16,8 @@ jobs: publish-provider-package: uses: crossplane-contrib/provider-workflows/.github/workflows/publish-provider-non-family.yml@main with: - repository: riccap + repository: provider-http + registry_org: riccap version: ${{ github.event.inputs.version }} go-version: ${{ github.event.inputs.go-version }} cleanup-disk: true