diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index 68634d8..e753750 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: version: - description: 'Release version (e.g. v0.2.0)' + description: 'Release version (e.g. v1.0.0)' required: true message: description: 'Tag message' diff --git a/README.md b/README.md index 21b2f23..5193cf5 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ To install `provider-http`, you have two options: 1. Using the Crossplane CLI in a Kubernetes cluster where Crossplane is installed: ```console - kubectl crossplane install provider xpkg.upbound.io/crossplane-contrib/provider-http:v0.2.0 + kubectl crossplane install provider xpkg.upbound.io/crossplane-contrib/provider-http:v1.0.0 ``` 2. Manually creating a Provider by applying the following YAML: @@ -39,7 +39,7 @@ To install `provider-http`, you have two options: Create a `DisposableRequest` resource to initiate a single-use HTTP interaction: ```yaml -apiVersion: http.crossplane.io/v1alpha1 +apiVersion: http.crossplane.io/v1alpha2 kind: DisposableRequest metadata: name: example-disposable-request @@ -54,7 +54,7 @@ For more detailed examples and configuration options, refer to the [examples dir Manage a resource through HTTP requests with a `Request` resource: ```yaml -apiVersion: http.crossplane.io/v1alpha1 +apiVersion: http.crossplane.io/v1alpha2 kind: Request metadata: name: example-request @@ -64,7 +64,7 @@ spec: For more detailed examples and configuration options, refer to the [examples directory](examples/sample/). -### Developing locally +## Developing locally Run controller against the cluster: ``` @@ -72,5 +72,5 @@ make run ``` -### Troubleshooting +## Troubleshooting If you encounter any issues during installation or usage, refer to the [troubleshooting guide](https://docs.crossplane.io/knowledge-base/guides/troubleshoot/) for common problems and solutions. \ No newline at end of file diff --git a/apis/disposablerequest/v1alpha2/disposablerequest_types.go b/apis/disposablerequest/v1alpha2/disposablerequest_types.go new file mode 100644 index 0000000..9b67df8 --- /dev/null +++ b/apis/disposablerequest/v1alpha2/disposablerequest_types.go @@ -0,0 +1,144 @@ +/* +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" + + xpv1 "github.com/crossplane/crossplane-runtime/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"` + + // SecretInjectionConfig specifies the secrets receiving patches for response data. + SecretInjectionConfigs []SecretInjectionConfig `json:"secretInjectionConfigs,omitempty"` +} + +// A DisposableRequestSpec defines the desired state of a DisposableRequest. +type DisposableRequestSpec struct { + xpv1.ResourceSpec `json:",inline"` + ForProvider DisposableRequestParameters `json:"forProvider"` +} + +// SecretInjectionConfig represents the configuration for injecting secret data into a Kubernetes secret. +type SecretInjectionConfig struct { + // SecretRef contains the name and namespace of the Kubernetes secret where the data will be injected. + SecretRef SecretRef `json:"secretRef"` + + // SecretKey is the key within the Kubernetes secret where the data will be injected. + SecretKey string `json:"secretKey"` + + // ResponsePath is is a jq filter expression represents the path in the response where the secret value will be extracted from. + ResponsePath string `json:"responsePath"` +} + +// SecretRef contains the name and namespace of a Kubernetes secret. +type SecretRef struct { + // Name is the name of the Kubernetes secret. + Name string `json:"name"` + + // Namespace is the namespace of the Kubernetes secret. + Namespace string `json:"namespace"` +} + +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"` +} + +// +kubebuilder:object:root=true + +// A DisposableRequest is an example API type. +// +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=Cluster,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/disposablerequest/v1alpha2/doc.go b/apis/disposablerequest/v1alpha2/doc.go new file mode 100644 index 0000000..af368a1 --- /dev/null +++ b/apis/disposablerequest/v1alpha2/doc.go @@ -0,0 +1,17 @@ +/* +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 diff --git a/apis/disposablerequest/v1alpha2/groupversion_info.go b/apis/disposablerequest/v1alpha2/groupversion_info.go new file mode 100644 index 0000000..8989b73 --- /dev/null +++ b/apis/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.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.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/disposablerequest/v1alpha2/status_setters.go b/apis/disposablerequest/v1alpha2/status_setters.go new file mode 100644 index 0000000..1eb31f3 --- /dev/null +++ b/apis/disposablerequest/v1alpha2/status_setters.go @@ -0,0 +1,34 @@ +package v1alpha2 + +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) SetError(err error) { + d.Status.Failed++ + d.Status.Synced = true + 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/disposablerequest/v1alpha2/zz_generated.deepcopy.go b/apis/disposablerequest/v1alpha2/zz_generated.deepcopy.go new file mode 100644 index 0000000..fc3c51b --- /dev/null +++ b/apis/disposablerequest/v1alpha2/zz_generated.deepcopy.go @@ -0,0 +1,257 @@ +//go:build !ignore_autogenerated +// +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 ( + "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 { + in, out := &val, &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.SecretInjectionConfigs != nil { + in, out := &in.SecretInjectionConfigs, &out.SecretInjectionConfigs + *out = make([]SecretInjectionConfig, len(*in)) + copy(*out, *in) + } +} + +// 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) +} + +// 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 { + in, out := &val, &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 { + in, out := &val, &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 +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretInjectionConfig) DeepCopyInto(out *SecretInjectionConfig) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretInjectionConfig. +func (in *SecretInjectionConfig) DeepCopy() *SecretInjectionConfig { + if in == nil { + return nil + } + out := new(SecretInjectionConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretRef) DeepCopyInto(out *SecretRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretRef. +func (in *SecretRef) DeepCopy() *SecretRef { + if in == nil { + return nil + } + out := new(SecretRef) + in.DeepCopyInto(out) + return out +} diff --git a/apis/disposablerequest/v1alpha2/zz_generated.managed.go b/apis/disposablerequest/v1alpha2/zz_generated.managed.go new file mode 100644 index 0000000..e20bf41 --- /dev/null +++ b/apis/disposablerequest/v1alpha2/zz_generated.managed.go @@ -0,0 +1,96 @@ +/* +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 +} + +// GetManagementPolicy of this DisposableRequest. +func (mg *DisposableRequest) GetManagementPolicy() xpv1.ManagementPolicy { + return mg.Spec.ManagementPolicy +} + +// GetProviderConfigReference of this DisposableRequest. +func (mg *DisposableRequest) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +/* +GetProviderReference of this DisposableRequest. +Deprecated: Use GetProviderConfigReference. +*/ +func (mg *DisposableRequest) GetProviderReference() *xpv1.Reference { + return mg.Spec.ProviderReference +} + +// 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 +} + +// SetManagementPolicy of this DisposableRequest. +func (mg *DisposableRequest) SetManagementPolicy(r xpv1.ManagementPolicy) { + mg.Spec.ManagementPolicy = r +} + +// SetProviderConfigReference of this DisposableRequest. +func (mg *DisposableRequest) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +/* +SetProviderReference of this DisposableRequest. +Deprecated: Use SetProviderConfigReference. +*/ +func (mg *DisposableRequest) SetProviderReference(r *xpv1.Reference) { + mg.Spec.ProviderReference = 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 +} diff --git a/apis/disposablerequest/v1alpha2/zz_generated.managedlist.go b/apis/disposablerequest/v1alpha2/zz_generated.managedlist.go new file mode 100644 index 0000000..c55775f --- /dev/null +++ b/apis/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/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 66d8b37..3e48468 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/v1alpha1" - requestv1alpha1 "github.com/crossplane-contrib/provider-http/apis/request/v1alpha1" + disposablerequestv1alpha1 "github.com/crossplane-contrib/provider-http/apis/disposablerequest/v1alpha2" + requestv1alpha1 "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpv1alpha1 "github.com/crossplane-contrib/provider-http/apis/v1alpha1" ) diff --git a/apis/request/v1alpha2/doc.go b/apis/request/v1alpha2/doc.go new file mode 100644 index 0000000..af368a1 --- /dev/null +++ b/apis/request/v1alpha2/doc.go @@ -0,0 +1,17 @@ +/* +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 diff --git a/apis/request/v1alpha2/groupversion_info.go b/apis/request/v1alpha2/groupversion_info.go new file mode 100644 index 0000000..8989b73 --- /dev/null +++ b/apis/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.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.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/request/v1alpha2/request_types.go b/apis/request/v1alpha2/request_types.go new file mode 100644 index 0000000..7bd4900 --- /dev/null +++ b/apis/request/v1alpha2/request_types.go @@ -0,0 +1,148 @@ +/* +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" + + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" +) + +// RequestParameters are the configurable fields of a Request. +type RequestParameters struct { + // Mappings defines the HTTP mappings for different methods. + 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 []SecretInjectionConfig `json:"secretInjectionConfigs,omitempty"` +} + +type Mapping struct { + // +kubebuilder:validation:Enum=POST;GET;PUT;DELETE + Method string `json:"method"` + Body string `json:"body,omitempty"` + URL string `json:"url"` + Headers map[string][]string `json:"headers,omitempty"` +} + +type Payload struct { + BaseUrl string `json:"baseUrl,omitempty"` + 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"` +} + +// SecretInjectionConfig represents the configuration for injecting secret data into a Kubernetes secret. +type SecretInjectionConfig struct { + // SecretRef contains the name and namespace of the Kubernetes secret where the data will be injected. + SecretRef SecretRef `json:"secretRef"` + + // SecretKey is the key within the Kubernetes secret where the data will be injected. + SecretKey string `json:"secretKey"` + + // ResponsePath is is a jq filter expression represents the path in the response where the secret value will be extracted from. + ResponsePath string `json:"responsePath"` +} + +// SecretRef contains the name and namespace of a Kubernetes secret. +type SecretRef struct { + // Name is the name of the Kubernetes secret. + Name string `json:"name"` + + // Namespace is the namespace of the Kubernetes secret. + Namespace string `json:"namespace"` +} + +// 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 an example API type. +// +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=Cluster,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/request/v1alpha2/status_setters.go b/apis/request/v1alpha2/status_setters.go new file mode 100644 index 0000000..bb4ae7f --- /dev/null +++ b/apis/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/request/v1alpha2/zz_generated.deepcopy.go b/apis/request/v1alpha2/zz_generated.deepcopy.go new file mode 100644 index 0000000..162be67 --- /dev/null +++ b/apis/request/v1alpha2/zz_generated.deepcopy.go @@ -0,0 +1,292 @@ +//go:build !ignore_autogenerated +// +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 ( + "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 *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 { + in, out := &val, &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 { + in, out := &val, &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([]SecretInjectionConfig, len(*in)) + copy(*out, *in) + } +} + +// 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 { + in, out := &val, &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 +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretInjectionConfig) DeepCopyInto(out *SecretInjectionConfig) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretInjectionConfig. +func (in *SecretInjectionConfig) DeepCopy() *SecretInjectionConfig { + if in == nil { + return nil + } + out := new(SecretInjectionConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretRef) DeepCopyInto(out *SecretRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretRef. +func (in *SecretRef) DeepCopy() *SecretRef { + if in == nil { + return nil + } + out := new(SecretRef) + in.DeepCopyInto(out) + return out +} diff --git a/apis/request/v1alpha2/zz_generated.managed.go b/apis/request/v1alpha2/zz_generated.managed.go new file mode 100644 index 0000000..d39724c --- /dev/null +++ b/apis/request/v1alpha2/zz_generated.managed.go @@ -0,0 +1,96 @@ +/* +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 +} + +// GetManagementPolicy of this Request. +func (mg *Request) GetManagementPolicy() xpv1.ManagementPolicy { + return mg.Spec.ManagementPolicy +} + +// GetProviderConfigReference of this Request. +func (mg *Request) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +/* +GetProviderReference of this Request. +Deprecated: Use GetProviderConfigReference. +*/ +func (mg *Request) GetProviderReference() *xpv1.Reference { + return mg.Spec.ProviderReference +} + +// 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 +} + +// SetManagementPolicy of this Request. +func (mg *Request) SetManagementPolicy(r xpv1.ManagementPolicy) { + mg.Spec.ManagementPolicy = r +} + +// SetProviderConfigReference of this Request. +func (mg *Request) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +/* +SetProviderReference of this Request. +Deprecated: Use SetProviderConfigReference. +*/ +func (mg *Request) SetProviderReference(r *xpv1.Reference) { + mg.Spec.ProviderReference = 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 +} diff --git a/apis/request/v1alpha2/zz_generated.managedlist.go b/apis/request/v1alpha2/zz_generated.managedlist.go new file mode 100644 index 0000000..565b460 --- /dev/null +++ b/apis/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/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/cluster/test/setup.sh b/cluster/test/setup.sh index 61cddfe..97e6188 100755 --- a/cluster/test/setup.sh +++ b/cluster/test/setup.sh @@ -24,7 +24,7 @@ metadata: labels: app: todo spec: - replicas: 1 # Number of replicas you want + replicas: 1 selector: matchLabels: app: todo @@ -55,4 +55,24 @@ spec: app: todo EOF +cat < to be shown at the status + Decrypted interface{} // Data containing sensitive data -> to be sent } type HttpRequest struct { @@ -41,14 +46,13 @@ type HttpDetails struct { HttpRequest HttpRequest } -func (hc *client) SendRequest(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (details HttpDetails, err error) { - requestBody := []byte(body) +func (hc *client) SendRequest(ctx context.Context, method string, url string, body Data, headers Data, skipTLSVerify bool) (details HttpDetails, err error) { + requestBody := []byte(body.Decrypted.(string)) request, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(requestBody)) - requestDetails := HttpRequest{ URL: url, - Body: body, - Headers: headers, + Body: body.Encrypted.(string), + Headers: headers.Encrypted.(map[string][]string), Method: method, } @@ -58,7 +62,7 @@ func (hc *client) SendRequest(ctx context.Context, method string, url string, bo }, err } - for key, values := range headers { + for key, values := range headers.Decrypted.(map[string][]string) { for _, value := range values { request.Header.Add(key, value) } diff --git a/internal/controller/disposablerequest/disposablerequest.go b/internal/controller/disposablerequest/disposablerequest.go index 5217494..ba46c22 100644 --- a/internal/controller/disposablerequest/disposablerequest.go +++ b/internal/controller/disposablerequest/disposablerequest.go @@ -22,13 +22,14 @@ import ( "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/pkg/logging" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/crossplane-contrib/provider-http/internal/jq" 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" @@ -37,7 +38,7 @@ import ( "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/pkg/resource" - "github.com/crossplane-contrib/provider-http/apis/disposablerequest/v1alpha1" + "github.com/crossplane-contrib/provider-http/apis/disposablerequest/v1alpha2" apisv1alpha1 "github.com/crossplane-contrib/provider-http/apis/v1alpha1" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane-contrib/provider-http/internal/utils" @@ -51,15 +52,22 @@ const ( 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 " ) // Setup adds a controller that reconciles DisposableRequest managed resources. func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error { - name := managed.ControllerName(v1alpha1.DisposableRequestGroupKind) + name := managed.ControllerName(v1alpha2.DisposableRequestGroupKind) cps := []managed.ConnectionPublisher{managed.NewAPISecretPublisher(mgr.GetClient(), mgr.GetScheme())} r := managed.NewReconciler(mgr, - resource.ManagedKind(v1alpha1.DisposableRequestGroupVersionKind), + resource.ManagedKind(v1alpha2.DisposableRequestGroupVersionKind), managed.WithExternalConnecter(&connector{ logger: o.Logger, kube: mgr.GetClient(), @@ -76,7 +84,7 @@ func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error Named(name). WithOptions(o.ForControllerRuntime()). WithEventFilter(resource.DesiredStateChanged()). - For(&v1alpha1.DisposableRequest{}). + For(&v1alpha2.DisposableRequest{}). Complete(ratelimiter.NewReconciler(name, r, o.GlobalRateLimiter)) } @@ -88,7 +96,7 @@ type connector struct { } func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { - cr, ok := mg.(*v1alpha1.DisposableRequest) + cr, ok := mg.(*v1alpha2.DisposableRequest) if !ok { return nil, errors.New(errNotDisposableRequest) } @@ -124,7 +132,7 @@ type external struct { } func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { - cr, ok := mg.(*v1alpha1.DisposableRequest) + cr, ok := mg.(*v1alpha2.DisposableRequest) if !ok { return managed.ExternalObservation{}, errors.New(errNotDisposableRequest) } @@ -137,7 +145,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex // 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, "failed to get the latest version of the resource") + return managed.ExternalObservation{}, errors.Wrap(err, errGetLatestVersion) } cr.Status.SetConditions(xpv1.Available()) @@ -152,11 +160,22 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex }, nil } -func (c *external) deployAction(ctx context.Context, cr *v1alpha1.DisposableRequest) error { - details, err := c.http.SendRequest(ctx, cr.Spec.ForProvider.Method, - cr.Spec.ForProvider.URL, cr.Spec.ForProvider.Body, cr.Spec.ForProvider.Headers, cr.Spec.ForProvider.InsecureSkipTLSVerify) +func (c *external) deployAction(ctx context.Context, cr *v1alpha2.DisposableRequest) error { + sensitiveBody, err := datapatcher.PatchSecretsIntoBody(ctx, c.localKube, cr.Spec.ForProvider.Body) + if err != nil { + return err + } - res := details.HttpResponse + sensitiveHeaders, err := datapatcher.PatchSecretsIntoHeaders(ctx, c.localKube, cr.Spec.ForProvider.Headers) + if err != nil { + return 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) + + sensitiveResponse := details.HttpResponse resource := &utils.RequestResource{ Resource: cr, RequestContext: ctx, @@ -165,9 +184,11 @@ func (c *external) deployAction(ctx context.Context, cr *v1alpha1.DisposableRequ HttpRequest: details.HttpRequest, } + c.patchResponseToSecret(ctx, cr, &resource.HttpResponse) + // 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 errors.Wrap(err, "failed to get the latest version of the resource") + return errors.Wrap(err, errGetLatestVersion) } if err != nil { @@ -178,15 +199,15 @@ func (c *external) deployAction(ctx context.Context, cr *v1alpha1.DisposableRequ return err } - if utils.IsHTTPError(res.StatusCode) { + if utils.IsHTTPError(resource.HttpResponse.StatusCode) { if settingError := utils.SetRequestResourceStatus(*resource, resource.SetStatusCode(), 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(res.StatusCode)) + return errors.Errorf(utils.ErrStatusCode, cr.Spec.ForProvider.Method, strconv.Itoa(resource.HttpResponse.StatusCode)) } - isExpectedResponse, err := c.isResponseAsExpected(cr, res) + isExpectedResponse, err := c.isResponseAsExpected(cr, sensitiveResponse) if err != nil { return err } @@ -194,13 +215,13 @@ func (c *external) deployAction(ctx context.Context, cr *v1alpha1.DisposableRequ if !isExpectedResponse { limit := utils.GetRollbackRetriesLimit(cr.Spec.ForProvider.RollbackRetriesLimit) return utils.SetRequestResourceStatus(*resource, resource.SetStatusCode(), resource.SetHeaders(), resource.SetBody(), - resource.SetError(errors.New("Response does not match the expected format, retries limit "+fmt.Sprint(limit))), resource.SetRequestDetails()) + resource.SetError(errors.New(errResponseFormat+fmt.Sprint(limit))), resource.SetRequestDetails()) } return utils.SetRequestResourceStatus(*resource, resource.SetStatusCode(), resource.SetHeaders(), resource.SetBody(), resource.SetSynced(), resource.SetRequestDetails()) } -func (c *external) isResponseAsExpected(cr *v1alpha1.DisposableRequest, res httpClient.HttpResponse) (bool, error) { +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 @@ -212,7 +233,7 @@ func (c *external) isResponseAsExpected(cr *v1alpha1.DisposableRequest, res http responseMap, err := json_util.StructToMap(res) if err != nil { - return false, errors.Wrap(err, "failed to convert response to map") + return false, errors.Wrap(err, errConvertResToMap) } json_util.ConvertJSONStringsToMaps(&responseMap) @@ -226,7 +247,7 @@ func (c *external) isResponseAsExpected(cr *v1alpha1.DisposableRequest, res http } func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { - cr, ok := mg.(*v1alpha1.DisposableRequest) + cr, ok := mg.(*v1alpha2.DisposableRequest) if !ok { return managed.ExternalCreation{}, errors.New(errNotDisposableRequest) } @@ -239,7 +260,7 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext } func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { - cr, ok := mg.(*v1alpha1.DisposableRequest) + cr, ok := mg.(*v1alpha2.DisposableRequest) if !ok { return managed.ExternalUpdate{}, errors.New(errNotDisposableRequest) } @@ -254,3 +275,12 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext func (c *external) Delete(_ context.Context, _ resource.Managed) error { return nil } + +func (c *external) patchResponseToSecret(ctx context.Context, cr *v1alpha2.DisposableRequest, response *httpClient.HttpResponse) { + for _, ref := range cr.Spec.ForProvider.SecretInjectionConfigs { + err := datapatcher.PatchResponseToSecret(ctx, c.localKube, c.logger, response, ref.ResponsePath, ref.SecretKey, ref.SecretRef.Name, ref.SecretRef.Namespace) + if err != nil { + c.logger.Info(fmt.Sprintf(errPatchDataToSecret, ref.SecretRef.Name, ref.SecretRef.Namespace, ref.SecretKey, err.Error())) + } + } +} diff --git a/internal/controller/disposablerequest/disposablerequest_test.go b/internal/controller/disposablerequest/disposablerequest_test.go index 95d32fc..9869248 100644 --- a/internal/controller/disposablerequest/disposablerequest_test.go +++ b/internal/controller/disposablerequest/disposablerequest_test.go @@ -22,8 +22,7 @@ import ( "testing" "time" - "github.com/crossplane-contrib/provider-http/apis/disposablerequest/v1alpha1" - + "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" @@ -72,21 +71,21 @@ const ( testBody = "{\"key1\": \"value1\"}" ) -type httpDisposableRequestModifier func(request *v1alpha1.DisposableRequest) +type httpDisposableRequestModifier func(request *v1alpha2.DisposableRequest) -func httpDisposableRequest(rm ...httpDisposableRequestModifier) *v1alpha1.DisposableRequest { - r := &v1alpha1.DisposableRequest{ +func httpDisposableRequest(rm ...httpDisposableRequestModifier) *v1alpha2.DisposableRequest { + r := &v1alpha2.DisposableRequest{ ObjectMeta: v1.ObjectMeta{ Name: testDisposableRequestName, Namespace: testNamespace, }, - Spec: v1alpha1.DisposableRequestSpec{ + Spec: v1alpha2.DisposableRequestSpec{ ResourceSpec: xpv1.ResourceSpec{ ProviderConfigReference: &xpv1.Reference{ Name: providerName, }, }, - ForProvider: v1alpha1.DisposableRequestParameters{ + ForProvider: v1alpha2.DisposableRequestParameters{ URL: testURL, Method: testMethod, Headers: testHeaders, @@ -94,7 +93,7 @@ func httpDisposableRequest(rm ...httpDisposableRequestModifier) *v1alpha1.Dispos WaitTimeout: testTimeout, }, }, - Status: v1alpha1.DisposableRequestStatus{}, + Status: v1alpha2.DisposableRequestStatus{}, } for _, m := range rm { @@ -104,13 +103,13 @@ func httpDisposableRequest(rm ...httpDisposableRequestModifier) *v1alpha1.Dispos return r } -type MockSendRequestFn func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) +type MockSendRequestFn func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) type MockHttpClient struct { MockSendRequest MockSendRequestFn } -func (c *MockHttpClient) SendRequest(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { +func (c *MockHttpClient) SendRequest(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { return c.MockSendRequest(ctx, method, url, body, headers, skipTLSVerify) } @@ -144,7 +143,7 @@ func Test_httpExternal_Create(t *testing.T) { "DisposableRequestFailed": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, errBoom }, }, @@ -162,7 +161,7 @@ func Test_httpExternal_Create(t *testing.T) { "Success": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, nil }, }, @@ -220,7 +219,7 @@ func Test_httpExternal_Update(t *testing.T) { "DisposableRequestFailed": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, errBoom }, }, @@ -237,7 +236,7 @@ func Test_httpExternal_Update(t *testing.T) { "Success": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, nil }, }, @@ -271,7 +270,7 @@ func Test_httpExternal_Update(t *testing.T) { func Test_deployAction(t *testing.T) { type args struct { - cr *v1alpha1.DisposableRequest + cr *v1alpha2.DisposableRequest http httpClient.Client localKube client.Client } @@ -291,7 +290,7 @@ func Test_deployAction(t *testing.T) { "SuccessUpdateStatusRequestFailure": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, errors.Errorf(utils.ErrInvalidURL, "invalid-url") }, }, @@ -299,16 +298,16 @@ func Test_deployAction(t *testing.T) { MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), MockGet: test.NewMockGetFn(nil), }, - cr: &v1alpha1.DisposableRequest{ - Spec: v1alpha1.DisposableRequestSpec{ - ForProvider: v1alpha1.DisposableRequestParameters{ + cr: &v1alpha2.DisposableRequest{ + Spec: v1alpha2.DisposableRequestSpec{ + ForProvider: v1alpha2.DisposableRequestParameters{ URL: "invalid-url", Method: testMethod, Headers: testHeaders, Body: testBody, }, }, - Status: v1alpha1.DisposableRequestStatus{}, + Status: v1alpha2.DisposableRequestStatus{}, }, }, want: want{ @@ -319,7 +318,7 @@ func Test_deployAction(t *testing.T) { "SuccessUpdateStatusCodeError": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + 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: 400, @@ -333,16 +332,16 @@ func Test_deployAction(t *testing.T) { MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), MockGet: test.NewMockGetFn(nil), }, - cr: &v1alpha1.DisposableRequest{ - Spec: v1alpha1.DisposableRequestSpec{ - ForProvider: v1alpha1.DisposableRequestParameters{ + cr: &v1alpha2.DisposableRequest{ + Spec: v1alpha2.DisposableRequestSpec{ + ForProvider: v1alpha2.DisposableRequestParameters{ URL: testURL, Method: testMethod, Headers: testHeaders, Body: testBody, }, }, - Status: v1alpha1.DisposableRequestStatus{}, + Status: v1alpha2.DisposableRequestStatus{}, }, }, want: want{ @@ -357,7 +356,7 @@ func Test_deployAction(t *testing.T) { "SuccessUpdateStatusSuccessfulRequest": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + 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, @@ -371,16 +370,16 @@ func Test_deployAction(t *testing.T) { MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), MockGet: test.NewMockGetFn(nil), }, - cr: &v1alpha1.DisposableRequest{ - Spec: v1alpha1.DisposableRequestSpec{ - ForProvider: v1alpha1.DisposableRequestParameters{ + cr: &v1alpha2.DisposableRequest{ + Spec: v1alpha2.DisposableRequestSpec{ + ForProvider: v1alpha2.DisposableRequestParameters{ URL: testURL, Method: testMethod, Headers: testHeaders, Body: testBody, }, }, - Status: v1alpha1.DisposableRequestStatus{}, + Status: v1alpha2.DisposableRequestStatus{}, }, }, want: want{ diff --git a/internal/controller/request/observe.go b/internal/controller/request/observe.go index 55609df..22d0afa 100644 --- a/internal/controller/request/observe.go +++ b/internal/controller/request/observe.go @@ -5,7 +5,7 @@ import ( "net/http" "strings" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha1" + "github.com/crossplane-contrib/provider-http/apis/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/json" @@ -43,12 +43,12 @@ func FailedObserve() ObserveRequestDetails { } // isUpToDate checks whether desired spec up to date with the observed state for a given request -func (c *external) isUpToDate(ctx context.Context, cr *v1alpha1.Request) (ObserveRequestDetails, error) { +func (c *external) isUpToDate(ctx context.Context, cr *v1alpha2.Request) (ObserveRequestDetails, error) { if !c.isObjectValidForObservation(cr) { return FailedObserve(), errors.New(errObjectNotFound) } - requestDetails, err := c.requestDetails(cr, http.MethodGet) + requestDetails, err := c.requestDetails(ctx, cr, http.MethodGet) if err != nil { return FailedObserve(), err } @@ -58,7 +58,8 @@ func (c *external) isUpToDate(ctx context.Context, cr *v1alpha1.Request) (Observ return FailedObserve(), errors.New(errObjectNotFound) } - desiredState, err := c.desiredState(cr) + c.patchResponseToSecret(ctx, cr, &details.HttpResponse) + desiredState, err := c.desiredState(ctx, cr) if err != nil { return FailedObserve(), err } @@ -66,7 +67,7 @@ func (c *external) isUpToDate(ctx context.Context, cr *v1alpha1.Request) (Observ return c.compareResponseAndDesiredState(details, responseErr, desiredState) } -func (c *external) isObjectValidForObservation(cr *v1alpha1.Request) bool { +func (c *external) isObjectValidForObservation(cr *v1alpha2.Request) bool { return cr.Status.Response.Body != "" && !(cr.Status.RequestDetails.Method == http.MethodPost && utils.IsHTTPError(cr.Status.Response.StatusCode)) } @@ -93,16 +94,16 @@ func (c *external) compareResponseAndDesiredState(details httpClient.HttpDetails return observeRequestDetails, nil } -func (c *external) desiredState(cr *v1alpha1.Request) (string, error) { - requestDetails, err := c.requestDetails(cr, http.MethodPut) - return requestDetails.Body, err +func (c *external) desiredState(ctx context.Context, cr *v1alpha2.Request) (string, error) { + requestDetails, err := c.requestDetails(ctx, cr, http.MethodPut) + return requestDetails.Body.Encrypted.(string), err } -func (c *external) requestDetails(cr *v1alpha1.Request, method string) (requestgen.RequestDetails, error) { +func (c *external) requestDetails(ctx context.Context, cr *v1alpha2.Request, method string) (requestgen.RequestDetails, error) { mapping, ok := getMappingByMethod(&cr.Spec.ForProvider, method) if !ok { return requestgen.RequestDetails{}, errors.Errorf(errMappingNotFound, method) } - return generateValidRequestDetails(cr, mapping) + return generateValidRequestDetails(ctx, c.localKube, cr, mapping) } diff --git a/internal/controller/request/observe_test.go b/internal/controller/request/observe_test.go index e9fcc0b..e122024 100644 --- a/internal/controller/request/observe_test.go +++ b/internal/controller/request/observe_test.go @@ -5,7 +5,7 @@ import ( "net/http" "testing" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha1" + "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" @@ -22,7 +22,7 @@ func Test_isUpToDate(t *testing.T) { type args struct { http httpClient.Client localKube client.Client - mg *v1alpha1.Request + mg *v1alpha2.Request } type want struct { result ObserveRequestDetails @@ -36,14 +36,14 @@ func Test_isUpToDate(t *testing.T) { "ObjectNotFoundEmptyBody": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, nil }, }, localKube: &test.MockClient{ MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), }, - mg: httpRequest(func(r *v1alpha1.Request) { + mg: httpRequest(func(r *v1alpha2.Request) { r.Status.Response.Body = "" }), }, @@ -54,14 +54,14 @@ func Test_isUpToDate(t *testing.T) { "ObjectNotFoundPostFailed": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, nil }, }, localKube: &test.MockClient{ MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), }, - mg: httpRequest(func(r *v1alpha1.Request) { + mg: httpRequest(func(r *v1alpha2.Request) { r.Status.RequestDetails.Method = http.MethodPost r.Status.Response.StatusCode = 400 }), @@ -73,14 +73,14 @@ func Test_isUpToDate(t *testing.T) { "ObjectNotFound404StatusCode": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, nil }, }, localKube: &test.MockClient{ MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), }, - mg: httpRequest(func(r *v1alpha1.Request) { + mg: httpRequest(func(r *v1alpha2.Request) { r.Status.Response.StatusCode = 404 }), }, @@ -91,7 +91,7 @@ func Test_isUpToDate(t *testing.T) { "FailBodyNotJSON": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + 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{ Body: "not a JSON", @@ -102,7 +102,7 @@ func Test_isUpToDate(t *testing.T) { localKube: &test.MockClient{ MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), }, - mg: httpRequest(func(r *v1alpha1.Request) { + mg: httpRequest(func(r *v1alpha2.Request) { r.Status.Response.Body = `{"username":"john_doe_new_username"}` }), }, @@ -113,7 +113,7 @@ func Test_isUpToDate(t *testing.T) { "SuccessNotSynced": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + 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{ Body: `{"username":"old_name"}`, @@ -125,7 +125,7 @@ func Test_isUpToDate(t *testing.T) { localKube: &test.MockClient{ MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), }, - mg: httpRequest(func(r *v1alpha1.Request) { + mg: httpRequest(func(r *v1alpha2.Request) { r.Status.Response.Body = `{"username":"john_doe_new_username"}` }), }, @@ -147,7 +147,7 @@ func Test_isUpToDate(t *testing.T) { "SuccessJSONBody": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + 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{ Body: `{"username":"john_doe_new_username"}`, @@ -159,7 +159,7 @@ func Test_isUpToDate(t *testing.T) { localKube: &test.MockClient{ MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), }, - mg: httpRequest(func(r *v1alpha1.Request) { + mg: httpRequest(func(r *v1alpha2.Request) { r.Status.Response.Body = `{"username":"john_doe_new_username"}` r.Status.Response.StatusCode = 200 }), diff --git a/internal/controller/request/request.go b/internal/controller/request/request.go index 3ab697f..9d38e9c 100644 --- a/internal/controller/request/request.go +++ b/internal/controller/request/request.go @@ -18,6 +18,7 @@ package request import ( "context" + "fmt" "net/http" "time" @@ -34,11 +35,12 @@ import ( "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/pkg/resource" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha1" + "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" apisv1alpha1 "github.com/crossplane-contrib/provider-http/apis/v1alpha1" 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/statushandler" + datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" "github.com/crossplane-contrib/provider-http/internal/utils" ) @@ -52,15 +54,17 @@ const ( errFailedToUpdateStatusFailures = "failed to reset status failures counter" errFailedUpdateStatusConditions = "failed updating status conditions" errMappingNotFound = "%s mapping doesn't exist in request, skipping operation" + 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" ) // Setup adds a controller that reconciles Request managed resources. func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error { - name := managed.ControllerName(v1alpha1.RequestGroupKind) + name := managed.ControllerName(v1alpha2.RequestGroupKind) cps := []managed.ConnectionPublisher{managed.NewAPISecretPublisher(mgr.GetClient(), mgr.GetScheme())} r := managed.NewReconciler(mgr, - resource.ManagedKind(v1alpha1.RequestGroupVersionKind), + resource.ManagedKind(v1alpha2.RequestGroupVersionKind), managed.WithExternalConnecter(&connector{ logger: o.Logger, kube: mgr.GetClient(), @@ -77,7 +81,7 @@ func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error Named(name). WithOptions(o.ForControllerRuntime()). WithEventFilter(resource.DesiredStateChanged()). - For(&v1alpha1.Request{}). + For(&v1alpha2.Request{}). Complete(ratelimiter.NewReconciler(name, r, o.GlobalRateLimiter)) } @@ -96,7 +100,7 @@ type connector struct { // 3. Getting the credentials specified by the ProviderConfig. // 4. Using the credentials to form a client. func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { - cr, ok := mg.(*v1alpha1.Request) + cr, ok := mg.(*v1alpha2.Request) if !ok { return nil, errors.New(errNotRequest) } @@ -134,7 +138,7 @@ type external struct { } func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { - cr, ok := mg.(*v1alpha1.Request) + cr, ok := mg.(*v1alpha2.Request) if !ok { return managed.ExternalObservation{}, errors.New(errNotRequest) } @@ -152,7 +156,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex // 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, "failed to get the latest version of the resource") + return managed.ExternalObservation{}, errors.Wrap(err, errGetLatestVersion) } statusHandler, err := statushandler.NewStatusHandler(ctx, cr, observeRequestDetails.Details, observeRequestDetails.ResponseError, c.localKube, c.logger) @@ -178,19 +182,20 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex }, nil } -func (c *external) deployAction(ctx context.Context, cr *v1alpha1.Request, method string) error { +func (c *external) deployAction(ctx context.Context, cr *v1alpha2.Request, method string) error { mapping, ok := getMappingByMethod(&cr.Spec.ForProvider, method) if !ok { - c.logger.Info(errMappingNotFound, method) + c.logger.Info(fmt.Sprintf(errMappingNotFound, method)) return nil } - requestDetails, err := generateValidRequestDetails(cr, mapping) + requestDetails, err := generateValidRequestDetails(ctx, c.localKube, cr, mapping) if err != nil { return err } details, err := c.http.SendRequest(ctx, mapping.Method, requestDetails.Url, requestDetails.Body, requestDetails.Headers, cr.Spec.ForProvider.InsecureSkipTLSVerify) + c.patchResponseToSecret(ctx, cr, &details.HttpResponse) statusHandler, err := statushandler.NewStatusHandler(ctx, cr, details, err, c.localKube, c.logger) if err != nil { @@ -201,7 +206,7 @@ func (c *external) deployAction(ctx context.Context, cr *v1alpha1.Request, metho } func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { - cr, ok := mg.(*v1alpha1.Request) + cr, ok := mg.(*v1alpha2.Request) if !ok { return managed.ExternalCreation{}, errors.New(errNotRequest) } @@ -210,7 +215,7 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext } func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { - cr, ok := mg.(*v1alpha1.Request) + cr, ok := mg.(*v1alpha2.Request) if !ok { return managed.ExternalUpdate{}, errors.New(errNotRequest) } @@ -219,7 +224,7 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext } func (c *external) Delete(ctx context.Context, mg resource.Managed) error { - cr, ok := mg.(*v1alpha1.Request) + cr, ok := mg.(*v1alpha2.Request) if !ok { return errors.New(errNotRequest) } @@ -227,18 +232,27 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { return errors.Wrap(c.deployAction(ctx, cr, http.MethodDelete), errFailedToSendHttpRequest) } +func (c *external) patchResponseToSecret(ctx context.Context, cr *v1alpha2.Request, response *httpClient.HttpResponse) { + for _, ref := range cr.Spec.ForProvider.SecretInjectionConfigs { + err := datapatcher.PatchResponseToSecret(ctx, c.localKube, c.logger, response, ref.ResponsePath, ref.SecretKey, ref.SecretRef.Name, ref.SecretRef.Namespace) + if err != nil { + c.logger.Info(fmt.Sprintf(errPatchDataToSecret, ref.SecretRef.Name, ref.SecretRef.Namespace, ref.SecretKey, err.Error())) + } + } +} + // 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(cr *v1alpha1.Request, mapping *v1alpha1.Mapping) (requestgen.RequestDetails, error) { - requestDetails, _, ok := requestgen.GenerateRequestDetails(*mapping, cr.Spec.ForProvider, cr.Status.Response) +func generateValidRequestDetails(ctx context.Context, localKube client.Client, cr *v1alpha2.Request, mapping *v1alpha2.Mapping) (requestgen.RequestDetails, error) { + requestDetails, _, ok := requestgen.GenerateRequestDetails(ctx, localKube, *mapping, cr.Spec.ForProvider, cr.Status.Response) if requestgen.IsRequestValid(requestDetails) && ok { return requestDetails, nil } - requestDetails, err, _ := requestgen.GenerateRequestDetails(*mapping, cr.Spec.ForProvider, cr.Status.Cache.Response) + requestDetails, err, _ := requestgen.GenerateRequestDetails(ctx, localKube, *mapping, cr.Spec.ForProvider, cr.Status.Cache.Response) if err != nil { return requestgen.RequestDetails{}, err } diff --git a/internal/controller/request/request_test.go b/internal/controller/request/request_test.go index fb4a1cc..cae539d 100644 --- a/internal/controller/request/request_test.go +++ b/internal/controller/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/v1alpha1" + "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" @@ -28,12 +28,12 @@ const ( ) var ( - testForProvider = v1alpha1.RequestParameters{ - Payload: v1alpha1.Payload{ + testForProvider = v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", BaseUrl: "https://api.example.com/users", }, - Mappings: []v1alpha1.Mapping{ + Mappings: []v1alpha2.Mapping{ testPostMapping, testGetMapping, testPutMapping, @@ -42,15 +42,15 @@ var ( } ) -type httpRequestModifier func(request *v1alpha1.Request) +type httpRequestModifier func(request *v1alpha2.Request) -func httpRequest(rm ...httpRequestModifier) *v1alpha1.Request { - r := &v1alpha1.Request{ +func httpRequest(rm ...httpRequestModifier) *v1alpha2.Request { + r := &v1alpha2.Request{ ObjectMeta: v1.ObjectMeta{ Name: testRequestName, Namespace: testNamespace, }, - Spec: v1alpha1.RequestSpec{ + Spec: v1alpha2.RequestSpec{ ResourceSpec: xpv1.ResourceSpec{ ProviderConfigReference: &xpv1.Reference{ Name: providerName, @@ -58,7 +58,7 @@ func httpRequest(rm ...httpRequestModifier) *v1alpha1.Request { }, ForProvider: testForProvider, }, - Status: v1alpha1.RequestStatus{}, + Status: v1alpha2.RequestStatus{}, } for _, m := range rm { @@ -72,13 +72,13 @@ type notHttpRequest struct { resource.Managed } -type MockSendRequestFn func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) +type MockSendRequestFn func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) type MockHttpClient struct { MockSendRequest MockSendRequestFn } -func (c *MockHttpClient) SendRequest(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { +func (c *MockHttpClient) SendRequest(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { return c.MockSendRequest(ctx, method, url, body, headers, skipTLSVerify) } @@ -86,7 +86,7 @@ type MockSetRequestStatusFn func() error type MockResetFailuresFn func() -type MockInitFn func(ctx context.Context, cr *v1alpha1.Request, res httpClient.HttpResponse) +type MockInitFn func(ctx context.Context, cr *v1alpha2.Request, res httpClient.HttpResponse) type MockStatusHandler struct { MockSetRequest MockSetRequestStatusFn @@ -97,7 +97,7 @@ func (s *MockStatusHandler) ResetFailures() { s.MockResetFailures() } -func (s *MockStatusHandler) SetRequestStatus(ctx context.Context, cr *v1alpha1.Request, res httpClient.HttpResponse, err error) error { +func (s *MockStatusHandler) SetRequestStatus(ctx context.Context, cr *v1alpha2.Request, res httpClient.HttpResponse, err error) error { return s.MockSetRequest() } @@ -126,7 +126,7 @@ func Test_httpExternal_Create(t *testing.T) { "RequestFailed": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, errBoom }, }, @@ -143,7 +143,7 @@ func Test_httpExternal_Create(t *testing.T) { "Success": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, nil }, }, @@ -201,7 +201,7 @@ func Test_httpExternal_Update(t *testing.T) { "RequestFailed": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, errBoom }, }, @@ -218,7 +218,7 @@ func Test_httpExternal_Update(t *testing.T) { "Success": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, nil }, }, @@ -276,7 +276,7 @@ func Test_httpExternal_Delete(t *testing.T) { "RequestFailed": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, errBoom }, }, @@ -293,7 +293,7 @@ func Test_httpExternal_Delete(t *testing.T) { "Success": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body string, headers map[string][]string, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, nil }, }, diff --git a/internal/controller/request/requestgen/request_generator.go b/internal/controller/request/requestgen/request_generator.go index 991021b..659bf50 100644 --- a/internal/controller/request/requestgen/request_generator.go +++ b/internal/controller/request/requestgen/request_generator.go @@ -1,13 +1,17 @@ package requestgen import ( + "context" "fmt" "strings" "github.com/pkg/errors" + "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha1" + "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane-contrib/provider-http/internal/controller/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" @@ -16,12 +20,12 @@ import ( type RequestDetails struct { Url string - Body string - Headers map[string][]string + Body httpClient.Data + Headers httpClient.Data } // GenerateRequestDetails generates request details. -func GenerateRequestDetails(methodMapping v1alpha1.Mapping, forProvider v1alpha1.RequestParameters, response v1alpha1.Response) (RequestDetails, error, bool) { +func GenerateRequestDetails(ctx context.Context, localKube client.Client, methodMapping v1alpha2.Mapping, forProvider v1alpha2.RequestParameters, response v1alpha2.Response) (RequestDetails, error, bool) { jqObject := generateRequestObject(forProvider, response) url, err := generateURL(methodMapping.URL, jqObject) if err != nil { @@ -32,22 +36,22 @@ func GenerateRequestDetails(methodMapping v1alpha1.Mapping, forProvider v1alpha1 return RequestDetails{}, errors.Errorf(utils.ErrInvalidURL, url), false } - body, err := generateBody(methodMapping.Body, jqObject) + bodyData, err := generateBody(ctx, localKube, methodMapping.Body, jqObject) if err != nil { return RequestDetails{}, err, false } - headers, err := generateHeaders(coalesceHeaders(methodMapping.Headers, forProvider.Headers), jqObject) + headersData, err := generateHeaders(ctx, localKube, coalesceHeaders(methodMapping.Headers, forProvider.Headers), jqObject) if err != nil { return RequestDetails{}, err, false } - return RequestDetails{Body: body, Url: url, Headers: headers}, nil, true + return RequestDetails{Body: bodyData, Url: url, Headers: headersData}, nil, true } // generateRequestObject 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 generateRequestObject(forProvider v1alpha1.RequestParameters, response v1alpha1.Response) map[string]interface{} { +func generateRequestObject(forProvider v1alpha2.RequestParameters, response v1alpha2.Response) map[string]interface{} { baseMap, _ := json_util.StructToMap(forProvider) statusMap, _ := json_util.StructToMap(map[string]interface{}{ "response": response, @@ -82,26 +86,45 @@ func generateURL(urlJQFilter string, jqObject map[string]interface{}) (string, e } // generateBody applies a mapping body to generate the request body. -func generateBody(mappingBody string, jqObject map[string]interface{}) (string, error) { +func generateBody(ctx context.Context, localKube client.Client, mappingBody string, jqObject map[string]interface{}) (httpClient.Data, error) { if mappingBody == "" { - return "", nil + return httpClient.Data{ + Encrypted: "", + Decrypted: "", + }, nil } jqQuery := requestprocessing.ConvertStringToJQQuery(mappingBody) body, err := requestprocessing.ApplyJQOnStr(jqQuery, jqObject) if err != nil { - return "", err + return httpClient.Data{}, err + } + + sensitiveBody, err := datapatcher.PatchSecretsIntoBody(ctx, localKube, body) + if err != nil { + return httpClient.Data{}, err } - return body, nil + return httpClient.Data{ + Encrypted: body, + Decrypted: sensitiveBody, + }, nil } // generateHeaders applies JQ queries to generate headers. -func generateHeaders(headers map[string][]string, jqObject map[string]interface{}) (map[string][]string, error) { +func generateHeaders(ctx context.Context, localKube client.Client, headers map[string][]string, jqObject map[string]interface{}) (httpClient.Data, error) { generatedHeaders, err := requestprocessing.ApplyJQOnMapStrings(headers, jqObject) if err != nil { - return nil, err + return httpClient.Data{}, err + } + + sensitiveHeaders, err := datapatcher.PatchSecretsIntoHeaders(ctx, localKube, generatedHeaders) + if err != nil { + return httpClient.Data{}, err } - return generatedHeaders, nil + return httpClient.Data{ + Encrypted: generatedHeaders, + Decrypted: sensitiveHeaders, + }, nil } diff --git a/internal/controller/request/requestgen/request_generator_test.go b/internal/controller/request/requestgen/request_generator_test.go index 16c38ce..cf92e39 100644 --- a/internal/controller/request/requestgen/request_generator_test.go +++ b/internal/controller/request/requestgen/request_generator_test.go @@ -1,10 +1,14 @@ package requestgen import ( + "context" "testing" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha1" + "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" + "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/crossplane/crossplane-runtime/pkg/test" "github.com/google/go-cmp/cmp" ) @@ -21,38 +25,38 @@ var testHeaders2 = map[string][]string{ } var ( - testPostMapping = v1alpha1.Mapping{ + testPostMapping = v1alpha2.Mapping{ Method: "POST", Body: "{ username: .payload.body.username, email: .payload.body.email }", URL: ".payload.baseUrl", Headers: testHeaders, } - testPutMapping = v1alpha1.Mapping{ + testPutMapping = v1alpha2.Mapping{ Method: "PUT", Body: "{ username: \"john_doe_new_username\" }", URL: "(.payload.baseUrl + \"/\" + .response.body.id)", Headers: testHeaders, } - testGetMapping = v1alpha1.Mapping{ + testGetMapping = v1alpha2.Mapping{ Method: "GET", URL: "(.payload.baseUrl + \"/\" + .response.body.id)", } - testDeleteMapping = v1alpha1.Mapping{ + testDeleteMapping = v1alpha2.Mapping{ Method: "DELETE", URL: "(.payload.baseUrl + \"/\" + .response.body.id)", } ) var ( - testForProvider = v1alpha1.RequestParameters{ - Payload: v1alpha1.Payload{ + testForProvider = v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", BaseUrl: "https://api.example.com/users", }, - Mappings: []v1alpha1.Mapping{ + Mappings: []v1alpha2.Mapping{ testPostMapping, testGetMapping, testPutMapping, @@ -63,10 +67,11 @@ var ( func Test_GenerateRequestDetails(t *testing.T) { type args struct { - methodMapping v1alpha1.Mapping - forProvider v1alpha1.RequestParameters - response v1alpha1.Response + methodMapping v1alpha2.Mapping + forProvider v1alpha2.RequestParameters + response v1alpha2.Response logger logging.Logger + localKube client.Client } type want struct { requestDetails RequestDetails @@ -81,14 +86,20 @@ func Test_GenerateRequestDetails(t *testing.T) { args: args{ methodMapping: testPostMapping, forProvider: testForProvider, - response: v1alpha1.Response{}, + response: v1alpha2.Response{}, logger: logging.NewNopLogger(), }, want: want{ requestDetails: RequestDetails{ - Url: "https://api.example.com/users", - Body: `{"email":"john.doe@example.com","username":"john_doe"}`, - Headers: testHeaders, + 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, @@ -98,7 +109,7 @@ func Test_GenerateRequestDetails(t *testing.T) { args: args{ methodMapping: testPutMapping, forProvider: testForProvider, - response: v1alpha1.Response{ + response: v1alpha2.Response{ StatusCode: 200, Body: `{"id":"123","username":"john_doe"}`, Headers: testHeaders, @@ -107,9 +118,15 @@ func Test_GenerateRequestDetails(t *testing.T) { }, want: want{ requestDetails: RequestDetails{ - Url: "https://api.example.com/users/123", - Body: `{"username":"john_doe_new_username"}`, - Headers: testHeaders, + 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, @@ -119,7 +136,7 @@ func Test_GenerateRequestDetails(t *testing.T) { args: args{ methodMapping: testDeleteMapping, forProvider: testForProvider, - response: v1alpha1.Response{ + response: v1alpha2.Response{ StatusCode: 200, Body: `{"id":"123","username":"john_doe"}`, Headers: testHeaders, @@ -128,8 +145,15 @@ func Test_GenerateRequestDetails(t *testing.T) { }, want: want{ requestDetails: RequestDetails{ - Url: "https://api.example.com/users/123", - Headers: map[string][]string{}, + 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, @@ -139,7 +163,7 @@ func Test_GenerateRequestDetails(t *testing.T) { args: args{ methodMapping: testGetMapping, forProvider: testForProvider, - response: v1alpha1.Response{ + response: v1alpha2.Response{ StatusCode: 200, Body: `{"id":"123","username":"john_doe"}`, Headers: testHeaders, @@ -148,8 +172,15 @@ func Test_GenerateRequestDetails(t *testing.T) { }, want: want{ requestDetails: RequestDetails{ - Url: "https://api.example.com/users/123", - Headers: map[string][]string{}, + 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, @@ -158,7 +189,7 @@ func Test_GenerateRequestDetails(t *testing.T) { } for name, tc := range cases { t.Run(name, func(t *testing.T) { - got, gotErr, ok := GenerateRequestDetails(tc.args.methodMapping, tc.args.forProvider, tc.args.response) + got, gotErr, ok := GenerateRequestDetails(context.Background(), tc.args.localKube, tc.args.methodMapping, tc.args.forProvider, tc.args.response) if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { t.Fatalf("GenerateRequestDetails(...): -want error, +got error: %s", diff) } @@ -189,9 +220,15 @@ func Test_IsRequestValid(t *testing.T) { "ValidRequestDetails": { args: args{ requestDetails: RequestDetails{ - Body: `{"id": "123", "username": "john_doe"}`, - Url: "https://example", - Headers: nil, + 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{ @@ -201,9 +238,15 @@ func Test_IsRequestValid(t *testing.T) { "NonValidRequestDetails": { args: args{ requestDetails: RequestDetails{ - Body: "", - Url: "", - Headers: nil, + Body: httpClient.Data{ + Encrypted: "", + Decrypted: "", + }, + Headers: httpClient.Data{ + Decrypted: nil, + Encrypted: nil, + }, + Url: "", }, }, want: want{ @@ -213,9 +256,15 @@ func Test_IsRequestValid(t *testing.T) { "NonValidUrl": { args: args{ requestDetails: RequestDetails{ - Body: `{"id": "123", "username": "john_doe"}`, - Url: "", - Headers: nil, + 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{ @@ -225,9 +274,15 @@ func Test_IsRequestValid(t *testing.T) { "NonValidBody": { args: args{ requestDetails: RequestDetails{ - Body: `{"id": "null", "username": "john_doe"}`, - Url: "https://example", - Headers: nil, + 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{ @@ -308,8 +363,8 @@ func Test_coalesceHeaders(t *testing.T) { func Test_generateRequestObject(t *testing.T) { type args struct { - forProvider v1alpha1.RequestParameters - response v1alpha1.Response + forProvider v1alpha2.RequestParameters + response v1alpha2.Response } type want struct { result map[string]interface{} @@ -321,7 +376,7 @@ func Test_generateRequestObject(t *testing.T) { "Success": { args: args{ forProvider: testForProvider, - response: v1alpha1.Response{ + response: v1alpha2.Response{ StatusCode: 200, Body: `{"id": "123"}`, Headers: nil, diff --git a/internal/controller/request/responseconverter/converter.go b/internal/controller/request/responseconverter/converter.go index 132d0ed..cc23745 100644 --- a/internal/controller/request/responseconverter/converter.go +++ b/internal/controller/request/responseconverter/converter.go @@ -1,13 +1,13 @@ package responseconverter import ( - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha1" + "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" ) // Convert HttpResponse to Response -func HttpResponseToV1alpha1Response(httpResponse httpClient.HttpResponse) v1alpha1.Response { - return v1alpha1.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/request/responseconverter/converter_test.go b/internal/controller/request/responseconverter/converter_test.go index 82a5794..48e0b44 100644 --- a/internal/controller/request/responseconverter/converter_test.go +++ b/internal/controller/request/responseconverter/converter_test.go @@ -3,7 +3,7 @@ package responseconverter import ( "testing" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha1" + "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/google/go-cmp/cmp" ) @@ -20,7 +20,7 @@ func Test_HttpResponseToV1alpha1Response(t *testing.T) { httpResponse httpClient.HttpResponse } type want struct { - result v1alpha1.Response + result v1alpha2.Response } cases := map[string]struct { args args @@ -35,7 +35,7 @@ func Test_HttpResponseToV1alpha1Response(t *testing.T) { }, }, want: want{ - result: v1alpha1.Response{ + result: v1alpha2.Response{ Body: `{"email":"john.doe@example.com","name":"john_doe"}`, Headers: testHeaders, StatusCode: 200, diff --git a/internal/controller/request/statushandler/status.go b/internal/controller/request/statushandler/status.go index 7444c56..3c4f017 100644 --- a/internal/controller/request/statushandler/status.go +++ b/internal/controller/request/statushandler/status.go @@ -5,7 +5,7 @@ import ( "net/http" "strconv" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha1" + "github.com/crossplane-contrib/provider-http/apis/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" @@ -16,7 +16,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -// RequestStatusHandler is the interface to interact with status setting for v1alpha1.Request +// RequestStatusHandler is the interface to interact with status setting for v1alpha2.Request type RequestStatusHandler interface { SetRequestStatus() error ResetFailures() @@ -29,7 +29,7 @@ type requestStatusHandler struct { extraSetters *[]utils.SetRequestStatusFunc resource *utils.RequestResource responseError error - forProvider v1alpha1.RequestParameters + forProvider v1alpha2.RequestParameters } // SetRequestStatus updates the current Request's status to reflect the details of the last HTTP request that occurred. @@ -83,7 +83,7 @@ func (r *requestStatusHandler) incrementFailuresAndReturn(combinedSetters []util return errors.Errorf(utils.ErrStatusCode, r.resource.HttpRequest.Method, strconv.Itoa(r.resource.HttpResponse.StatusCode)) } -func (r *requestStatusHandler) appendExtraSetters(forProvider v1alpha1.RequestParameters, combinedSetters *[]utils.SetRequestStatusFunc) { +func (r *requestStatusHandler) appendExtraSetters(forProvider v1alpha2.RequestParameters, combinedSetters *[]utils.SetRequestStatusFunc) { if r.resource.HttpRequest.Method != http.MethodGet { *combinedSetters = append(*combinedSetters, r.resource.ResetFailures()) } @@ -96,10 +96,10 @@ func (r *requestStatusHandler) appendExtraSetters(forProvider v1alpha1.RequestPa // 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 v1alpha1.RequestParameters) bool { +func (r *requestStatusHandler) shouldSetCache(forProvider v1alpha2.RequestParameters) bool { for _, mapping := range forProvider.Mappings { response := responseconverter.HttpResponseToV1alpha1Response(r.resource.HttpResponse) - requestDetails, _, ok := requestgen.GenerateRequestDetails(mapping, forProvider, response) + requestDetails, _, ok := requestgen.GenerateRequestDetails(r.resource.RequestContext, r.resource.LocalClient, mapping, forProvider, response) if !(requestgen.IsRequestValid(requestDetails) && ok) { return false } @@ -117,7 +117,7 @@ func (r *requestStatusHandler) ResetFailures() { } // NewClient returns a new Request statusHandler -func NewStatusHandler(ctx context.Context, cr *v1alpha1.Request, requestDetails httpClient.HttpDetails, err error, localKube client.Client, logger logging.Logger) (RequestStatusHandler, error) { +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") diff --git a/internal/controller/request/statushandler/status_test.go b/internal/controller/request/statushandler/status_test.go index cb6deba..480def3 100644 --- a/internal/controller/request/statushandler/status_test.go +++ b/internal/controller/request/statushandler/status_test.go @@ -7,7 +7,7 @@ import ( "github.com/pkg/errors" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha1" + "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane-contrib/provider-http/internal/utils" "github.com/crossplane/crossplane-runtime/pkg/logging" @@ -32,36 +32,36 @@ const ( ) var ( - testPostMapping = v1alpha1.Mapping{ + testPostMapping = v1alpha2.Mapping{ Method: "POST", Body: "{ username: .payload.body.username, email: .payload.body.email }", URL: ".payload.baseUrl", } - testPutMapping = v1alpha1.Mapping{ + testPutMapping = v1alpha2.Mapping{ Method: "PUT", Body: "{ username: \"john_doe_new_username\" }", URL: "(.payload.baseUrl + \"/\" + .response.body.id)", } - testGetMapping = v1alpha1.Mapping{ + testGetMapping = v1alpha2.Mapping{ Method: "GET", URL: "(.payload.baseUrl + \"/\" + .response.body.id)", } - testDeleteMapping = v1alpha1.Mapping{ + testDeleteMapping = v1alpha2.Mapping{ Method: "DELETE", URL: "(.payload.baseUrl + \"/\" + .response.body.id)", } ) var ( - testForProvider = v1alpha1.RequestParameters{ - Payload: v1alpha1.Payload{ + testForProvider = v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", BaseUrl: "https://api.example.com/users", }, - Mappings: []v1alpha1.Mapping{ + Mappings: []v1alpha2.Mapping{ testPostMapping, testGetMapping, testPutMapping, @@ -70,8 +70,8 @@ var ( } ) -var testCr = &v1alpha1.Request{ - Spec: v1alpha1.RequestSpec{ +var testCr = &v1alpha2.Request{ + Spec: v1alpha2.RequestSpec{ ForProvider: testForProvider, }, } @@ -85,7 +85,7 @@ var testRequest = httpClient.HttpRequest{ func Test_SetRequestStatus(t *testing.T) { type args struct { localKube client.Client - cr *v1alpha1.Request + cr *v1alpha2.Request requestDetails httpClient.HttpDetails err error isSynced bool diff --git a/internal/controller/request/utils.go b/internal/controller/request/utils.go index 5c1a75f..65ba4b8 100644 --- a/internal/controller/request/utils.go +++ b/internal/controller/request/utils.go @@ -1,10 +1,10 @@ package request import ( - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha1" + "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" ) -func getMappingByMethod(requestParams *v1alpha1.RequestParameters, method string) (*v1alpha1.Mapping, bool) { +func getMappingByMethod(requestParams *v1alpha2.RequestParameters, method string) (*v1alpha2.Mapping, bool) { for _, mapping := range requestParams.Mappings { if mapping.Method == method { return &mapping, true diff --git a/internal/controller/request/utils_test.go b/internal/controller/request/utils_test.go index d81bfed..c5d41df 100644 --- a/internal/controller/request/utils_test.go +++ b/internal/controller/request/utils_test.go @@ -3,29 +3,29 @@ package request import ( "testing" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha1" + "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" "github.com/google/go-cmp/cmp" ) var ( - testPostMapping = v1alpha1.Mapping{ + testPostMapping = v1alpha2.Mapping{ Method: "POST", Body: "{ username: .payload.body.username, email: .payload.body.email }", URL: ".payload.baseUrl", } - testPutMapping = v1alpha1.Mapping{ + testPutMapping = v1alpha2.Mapping{ Method: "PUT", Body: "{ username: \"john_doe_new_username\" }", URL: "(.payload.baseUrl + \"/\" + .response.body.id)", } - testGetMapping = v1alpha1.Mapping{ + testGetMapping = v1alpha2.Mapping{ Method: "GET", URL: "(.payload.baseUrl + \"/\" + .response.body.id)", } - testDeleteMapping = v1alpha1.Mapping{ + testDeleteMapping = v1alpha2.Mapping{ Method: "DELETE", URL: "(.payload.baseUrl + \"/\" + .response.body.id)", } @@ -33,11 +33,11 @@ var ( func Test_getMappingByMethod(t *testing.T) { type args struct { - requestParams *v1alpha1.RequestParameters + requestParams *v1alpha2.RequestParameters method string } type want struct { - mapping *v1alpha1.Mapping + mapping *v1alpha2.Mapping ok bool } cases := map[string]struct { @@ -46,12 +46,12 @@ func Test_getMappingByMethod(t *testing.T) { }{ "Fail": { args: args{ - requestParams: &v1alpha1.RequestParameters{ - Payload: v1alpha1.Payload{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", BaseUrl: "https://api.example.com/users", }, - Mappings: []v1alpha1.Mapping{ + Mappings: []v1alpha2.Mapping{ testGetMapping, testPutMapping, testDeleteMapping, @@ -66,12 +66,12 @@ func Test_getMappingByMethod(t *testing.T) { }, "Success": { args: args{ - requestParams: &v1alpha1.RequestParameters{ - Payload: v1alpha1.Payload{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", BaseUrl: "https://api.example.com/users", }, - Mappings: []v1alpha1.Mapping{ + Mappings: []v1alpha2.Mapping{ testPostMapping, testGetMapping, testPutMapping, diff --git a/internal/data-patcher/parser.go b/internal/data-patcher/parser.go new file mode 100644 index 0000000..7a3e1c0 --- /dev/null +++ b/internal/data-patcher/parser.go @@ -0,0 +1,133 @@ +package datapatcher + +import ( + "fmt" + "regexp" + "strings" + + "strconv" + + "context" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" + "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/pkg/errors" +) + +const ( + errEmptyKey = "Warning, value at field %s is empty, skipping secret update for: %s" + errConvertData = "failed to convert data to map" +) + +const ( + secretPattern = `\{\{\s*([^:{}\s]+):([^:{}\s]+):([^:{}\s]+)\s*\}\}` +) + +var re = regexp.MustCompile(secretPattern) + +// findPlaceholders finds all placeholders in the provided string. +func findPlaceholders(value string) []string { + return re.FindAllString(value, -1) +} + +// removeDuplicates removes duplicate strings from the given slice. +func removeDuplicates(strSlice []string) []string { + unique := make(map[string]struct{}) + var result []string + + for _, str := range strSlice { + if _, ok := unique[str]; !ok { + result = append(result, str) + unique[str] = struct{}{} + } + } + + return result +} + +// parsePlaceholder parses a placeholder string and returns its components. +func parsePlaceholder(placeholder string) (name, namespace, key string, ok bool) { + matches := re.FindStringSubmatch(placeholder) + + if len(matches) != 4 { + return "", "", "", false + } + + return matches[1], matches[2], matches[3], true +} + +// replacePlaceholderWithSecretValue replaces a placeholder with the value from a secret. +func replacePlaceholderWithSecretValue(originalString, old string, secret *corev1.Secret, key string) string { + replacementString := string(secret.Data[key]) + return strings.ReplaceAll(originalString, old, replacementString) +} + +// patchSecretsToValue patches secrets referenced in the provided value. +func patchSecretsToValue(ctx context.Context, localKube client.Client, valueToHandle string) (string, error) { + placeholders := removeDuplicates(findPlaceholders(valueToHandle)) + for _, placeholder := range placeholders { + + name, namespace, key, ok := parsePlaceholder(placeholder) + if !ok { + return valueToHandle, nil + } + secret, err := kubehandler.GetSecret(ctx, localKube, name, namespace) + if err != nil { + return "", err + } + + valueToHandle = replacePlaceholderWithSecretValue(valueToHandle, placeholder, secret, key) + } + + return valueToHandle, nil + +} + +// patchValueToSecret patches a value to a secret. +func patchValueToSecret(ctx context.Context, kubeClient client.Client, logger logging.Logger, data *httpClient.HttpResponse, secret *corev1.Secret, secretKey string, requestFieldPath string) error { + dataMap, err := json_util.StructToMap(data) + if err != nil { + return errors.Wrap(err, errConvertData) + } + + json_util.ConvertJSONStringsToMaps(&dataMap) + + valueToPatch, err := jq.ParseString(requestFieldPath, dataMap) + if err != nil { + boolResult, err := jq.ParseBool(requestFieldPath, dataMap) + if err != nil { + valueToPatch = "" + } else { + valueToPatch = strconv.FormatBool(boolResult) + } + } + + if valueToPatch == "" { + logger.Info(fmt.Sprintf(errEmptyKey, requestFieldPath, fmt.Sprint(data))) + return nil + } + + if secret.Data == nil { + secret.Data = make(map[string][]byte) + } + + secret.Data[secretKey] = []byte(valueToPatch) + + // patch the {{name:namespace:key}} of secret instead of the sensitive value + placeholder := fmt.Sprintf("{{%s:%s:%s}}", secret.Name, secret.Namespace, secretKey) + data.Body = strings.ReplaceAll(data.Body, valueToPatch, placeholder) + for _, headersList := range data.Headers { + for i, header := range headersList { + newHeader := strings.ReplaceAll(header, valueToPatch, placeholder) + headersList[i] = newHeader + } + } + + return kubehandler.UpdateSecret(ctx, kubeClient, secret) +} diff --git a/internal/data-patcher/parser_test.go b/internal/data-patcher/parser_test.go new file mode 100644 index 0000000..9c639a1 --- /dev/null +++ b/internal/data-patcher/parser_test.go @@ -0,0 +1,306 @@ +package datapatcher + +import ( + "context" + "errors" + "testing" + + "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func createSpecificSecret(name, namespace, key, value string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: map[string][]byte{ + key: []byte(value), + "other-key": []byte("otherSecretValue"), + }, + } +} + +func Test_findPlaceholders(t *testing.T) { + type args struct { + value string + } + type want struct { + result []string + } + + cases := map[string]struct { + args args + want want + }{ + "ShouldFindPlaceholders": { + args: args{ + value: "data -> {{name:namespace:key}} {{name:namespace:key}} {{name-second:namespace-second:key-second}}", + }, + want: want{ + result: []string{"{{name:namespace:key}}", "{{name:namespace:key}}", "{{name-second:namespace-second:key-second}}"}, + }, + }, + } + for name, tc := range cases { + tc := tc // Create local copies of loop variables + + t.Run(name, func(t *testing.T) { + got := findPlaceholders(tc.args.value) + if diff := cmp.Diff(tc.want.result, got); diff != "" { + t.Errorf("findPlaceholders(...): -want result, +got result: %s", diff) + } + }) + } +} + +func Test_removeDuplicates(t *testing.T) { + type args struct { + value []string + } + type want struct { + result []string + } + + cases := map[string]struct { + args args + want want + }{ + "ShouldRemoveDuplicates": { + args: args{ + value: []string{"{{name:namespace:key}}", "{{name:namespace:key}}", "{{name-second:namespace-second:key-second}}"}, + }, + want: want{ + result: []string{"{{name:namespace:key}}", "{{name-second:namespace-second:key-second}}"}, + }, + }, + } + for name, tc := range cases { + tc := tc // Create local copies of loop variables + + t.Run(name, func(t *testing.T) { + got := removeDuplicates(tc.args.value) + if diff := cmp.Diff(tc.want.result, got); diff != "" { + t.Errorf("removeDuplicates(...): -want result, +got result: %s", diff) + } + }) + } +} + +func Test_parsePlaceholder(t *testing.T) { + type args struct { + placeholder string + } + type want struct { + name string + namespace string + key string + ok bool + } + + cases := map[string]struct { + args args + want want + }{ + "ShouldParsePlaceholder": { + args: args{ + placeholder: "{{name:namespace:key}}", + }, + want: want{ + name: "name", + namespace: "namespace", + key: "key", + ok: true, + }, + }, + "ShouldFailDueToInvalidSyntax": { + args: args{ + placeholder: "{{name::namespace:key}}", + }, + want: want{ + name: "", + namespace: "", + key: "", + ok: false, + }, + }, + "ShouldFailDueToLessArguments": { + args: args{ + placeholder: "{{name:key}}", + }, + want: want{ + name: "", + namespace: "", + key: "", + ok: false, + }, + }, + "ShouldFailDueToMoreArguments": { + args: args{ + placeholder: "{{name:key:namespace:try}}", + }, + want: want{ + name: "", + namespace: "", + key: "", + ok: false, + }, + }, + } + for name, tc := range cases { + tc := tc // Create local copies of loop variables + + t.Run(name, func(t *testing.T) { + name, namespace, key, ok := parsePlaceholder(tc.args.placeholder) + if diff := cmp.Diff(tc.want.name, name); diff != "" { + t.Errorf("parsePlaceholder(...): -want name, +got name: %s", diff) + } + if diff := cmp.Diff(tc.want.namespace, namespace); diff != "" { + t.Errorf("parsePlaceholder(...): -want namespace, +got namespace: %s", diff) + } + if diff := cmp.Diff(tc.want.key, key); diff != "" { + t.Errorf("parsePlaceholder(...): -want key, +got key: %s", diff) + } + if diff := cmp.Diff(tc.want.ok, ok); diff != "" { + t.Errorf("parsePlaceholder(...): -want ok, +got ok: %s", diff) + } + }) + } +} + +func Test_replacePlaceholderWithSecretValue(t *testing.T) { + type args struct { + originalString string + old string + secret *corev1.Secret + key string + } + type want struct { + result string + } + + cases := map[string]struct { + args args + want want + }{ + "ShouldGetSecret": { + args: args{ + originalString: "this is the test string", + old: "test", + secret: &corev1.Secret{ + Data: map[string][]byte{ + "key": []byte("changed"), + }, + }, + key: "key", + }, + want: want{ + result: "this is the changed string", + }, + }, + } + for name, tc := range cases { + tc := tc // Create local copies of loop variables + + t.Run(name, func(t *testing.T) { + got := replacePlaceholderWithSecretValue(tc.args.originalString, tc.args.old, tc.args.secret, tc.args.key) + if diff := cmp.Diff(tc.want.result, got); diff != "" { + t.Errorf("replacePlaceholderWithSecretValue(...): -want result, +got result: %s", diff) + } + }) + } +} + +func Test_patchSecretsToValue(t *testing.T) { + placeholderData := "{{name:namespace:key}}" + + type args struct { + valueToHandle string + localKube client.Client + } + + type want struct { + result string + } + + cases := map[string]struct { + args args + want want + }{ + "ShouldPatchSingleSecret": { + args: args{ + valueToHandle: "data -> " + placeholderData, + localKube: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + secret, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("object is not a Secret") + } + + *secret = *createSpecificSecret("name", "namespace", "key", "value") + return nil + }, + }, + }, + want: want{ + result: "data -> value", + }, + }, + "ShouldPatchMultipleSecrets": { + args: args{ + valueToHandle: "data -> " + placeholderData + " " + placeholderData, + localKube: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + secret, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("object is not a Secret") + } + + *secret = *createSpecificSecret("name", "namespace", "key", "value") + return nil + }, + }, + }, + want: want{ + result: "data -> value value", + }, + }, + "ShouldNotPatchInvalidPlaceholder": { + args: args{ + valueToHandle: "data -> {{invalid-placeholder}}", + localKube: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + secret, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("object is not a Secret") + } + + *secret = *createSpecificSecret("name", "namespace", "key", "value") + return nil + }, + }, + }, + want: want{ + result: "data -> {{invalid-placeholder}}", + }, + }, + } + + for name, tc := range cases { + tc := tc // Create local copies of loop variables + + t.Run(name, func(t *testing.T) { + got, err := patchSecretsToValue(context.Background(), tc.args.localKube, tc.args.valueToHandle) + if err != nil { + t.Fatalf("patchSecretsToValue(...): unexpected error: %v", err) + } + if got != tc.want.result { + t.Errorf("patchSecretsToValue(...): want result %q, got %q", tc.want.result, got) + } + }) + } +} diff --git a/internal/data-patcher/patch.go b/internal/data-patcher/patch.go new file mode 100644 index 0000000..abc319d --- /dev/null +++ b/internal/data-patcher/patch.go @@ -0,0 +1,66 @@ +package datapatcher + +import ( + "context" + + 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/pkg/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + errPatchToReferencedSecret = "cannot patch to referenced secret" +) + +// PatchSecretsIntoBody patches secrets into the provided string body. +func PatchSecretsIntoBody(ctx context.Context, localKube client.Client, body string) (string, error) { + return patchSecretsToValue(ctx, localKube, body) +} + +// PatchSecretsIntoHeaders takes a map of headers and applies security measures to +// sensitive values within the headers. It creates a copy of the input map +// to avoid modifying the original map and iterates over the copied map +// to process each list of headers. It then applies the necessary modifications +// to each header using patchSecretsToValue function. +func PatchSecretsIntoHeaders(ctx context.Context, localKube client.Client, headers map[string][]string) (map[string][]string, error) { + headersCopy := copyHeaders(headers) + + for _, headersList := range headersCopy { + for i, header := range headersList { + newHeader, err := patchSecretsToValue(ctx, localKube, header) + if err != nil { + return nil, err + } + + headersList[i] = newHeader + } + } + return headersCopy, nil +} + +// copyHeaders creates a deep copy of the provided headers map. +func copyHeaders(headers map[string][]string) map[string][]string { + headersCopy := make(map[string][]string, len(headers)) + for key, value := range headers { + headersCopy[key] = append([]string(nil), value...) + } + + return headersCopy +} + +// PatchResponseToSecret patches response data into a Kubernetes secret. +func PatchResponseToSecret(ctx context.Context, localKube client.Client, logger logging.Logger, data *httpClient.HttpResponse, path, secretKey, secretName, secretNamespace string) error { + secret, err := kubehandler.GetOrCreateSecret(ctx, localKube, secretName, secretNamespace) + if err != nil { + return err + } + + err = patchValueToSecret(ctx, localKube, logger, data, secret, secretKey, path) + if err != nil { + return errors.Wrap(err, errPatchToReferencedSecret) + } + + return nil +} diff --git a/internal/data-patcher/patch_test.go b/internal/data-patcher/patch_test.go new file mode 100644 index 0000000..6324e09 --- /dev/null +++ b/internal/data-patcher/patch_test.go @@ -0,0 +1,196 @@ +package datapatcher + +import ( + "context" + "testing" + + "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestPatchSecretsIntoBody(t *testing.T) { + type args struct { + ctx context.Context + localKube client.Client + body string + } + + type want struct { + result string + err error + } + + cases := map[string]struct { + args args + want want + }{ + "ShouldPatchSecretsIntoBody": { + args: args{ + ctx: context.Background(), + localKube: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + secret, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("object is not a Secret") + } + + *secret = *createSpecificSecret("name", "namespace", "key", "value") + return nil + }, + }, + body: "body with secrets: {{name:namespace:key}}", + }, + want: want{ + result: "body with secrets: value", + err: nil, + }, + }, + "ShouldNotPatchInvalidPlaceholder": { + args: args{ + ctx: context.Background(), + localKube: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + secret, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("object is not a Secret") + } + + *secret = *createSpecificSecret("name", "namespace", "key", "value") + return nil + }, + }, + body: "body with invalid placeholder: {{invalid-placeholder}}", + }, + want: want{ + result: "body with invalid placeholder: {{invalid-placeholder}}", + err: nil, + }, + }, + } + + for name, tc := range cases { + tc := tc // Create local copies of loop variables + + t.Run(name, func(t *testing.T) { + got, gotErr := PatchSecretsIntoBody(tc.args.ctx, tc.args.localKube, tc.args.body) + 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.result, got); diff != "" { + t.Errorf("isUpToDate(...): -want result, +got result: %s", diff) + } + }) + } +} +func TestPatchSecretsIntoHeaders(t *testing.T) { + type args struct { + ctx context.Context + localKube client.Client + headers map[string][]string + } + + type want struct { + result map[string][]string + err error + } + + cases := map[string]struct { + args args + want want + }{ + "ShouldPatchSingleHeader": { + args: args{ + ctx: context.Background(), + localKube: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + secret, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("object is not a Secret") + } + + *secret = *createSpecificSecret("name", "namespace", "key", "value") + return nil + }, + }, + headers: map[string][]string{ + "Authorization": {"Bearer {{name:namespace:key}}"}, + }, + }, + want: want{ + result: map[string][]string{ + "Authorization": {"Bearer value"}, + }, + err: nil, + }, + }, + "ShouldPatchMultipleHeaders": { + args: args{ + ctx: context.Background(), + localKube: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + secret, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("object is not a Secret") + } + + *secret = *createSpecificSecret("name", "namespace", "key", "value") + return nil + }, + }, + headers: map[string][]string{ + "Authorization": {"Bearer {{name:namespace:key}}"}, + "X-Secret": {"{{name:namespace:other-key}}"}, + }, + }, + want: want{ + result: map[string][]string{ + "Authorization": {"Bearer value"}, + "X-Secret": {"otherSecretValue"}, + }, + err: nil, + }, + }, + "ShouldNotPatchInvalidPlaceholder": { + args: args{ + ctx: context.Background(), + localKube: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + secret, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("object is not a Secret") + } + + *secret = *createSpecificSecret("name", "namespace", "key", "value") + return nil + }, + }, + headers: map[string][]string{ + "Authorization": {"Bearer {{invalid-placeholder}}"}, + }, + }, + want: want{ + result: map[string][]string{ + "Authorization": {"Bearer {{invalid-placeholder}}"}, + }, + err: nil, + }, + }, + } + + for name, tc := range cases { + tc := tc // Create local copies of loop variables + + t.Run(name, func(t *testing.T) { + got, gotErr := PatchSecretsIntoHeaders(tc.args.ctx, tc.args.localKube, tc.args.headers) + 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.result, got); diff != "" { + t.Errorf("isUpToDate(...): -want result, +got result: %s", diff) + } + }) + } +} diff --git a/internal/json/util.go b/internal/json/util.go index 88a4e01..70b7301 100644 --- a/internal/json/util.go +++ b/internal/json/util.go @@ -55,6 +55,15 @@ func StructToMap(obj interface{}) (newMap map[string]interface{}, err error) { return } +func ConvertMapToJson(m map[string]interface{}) ([]byte, bool) { + jsonData, err := json.Marshal(m) + if err != nil { + return nil, false + } + + return jsonData, true +} + func deepEqual(a, b interface{}) bool { aBytes, err := json.Marshal(a) if err != nil { diff --git a/internal/json/util_test.go b/internal/json/util_test.go index 6da1134..892e22b 100644 --- a/internal/json/util_test.go +++ b/internal/json/util_test.go @@ -3,43 +3,43 @@ package json import ( "testing" - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha1" + "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" "github.com/google/go-cmp/cmp" ) var ( - testPostMapping = v1alpha1.Mapping{ + testPostMapping = v1alpha2.Mapping{ Method: "POST", Body: "{ username: .payload.body.username, email: .payload.body.email }", URL: ".payload.baseUrl", // Headers: testHeaders, } - testPutMapping = v1alpha1.Mapping{ + testPutMapping = v1alpha2.Mapping{ Method: "PUT", Body: "{ username: \"john_doe_new_username\" }", URL: "(.payload.baseUrl + \"/\" + .response.body.id)", // Headers: testHeaders, } - testGetMapping = v1alpha1.Mapping{ + testGetMapping = v1alpha2.Mapping{ Method: "GET", URL: "(.payload.baseUrl + \"/\" + .response.body.id)", } - testDeleteMapping = v1alpha1.Mapping{ + testDeleteMapping = v1alpha2.Mapping{ Method: "DELETE", URL: "(.payload.baseUrl + \"/\" + .response.body.id)", } ) var ( - testForProvider = v1alpha1.RequestParameters{ - Payload: v1alpha1.Payload{ + testForProvider = v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", BaseUrl: "https://api.example.com/users", }, - Mappings: []v1alpha1.Mapping{ + Mappings: []v1alpha2.Mapping{ testPostMapping, testGetMapping, testPutMapping, diff --git a/internal/kube-handler/client.go b/internal/kube-handler/client.go new file mode 100644 index 0000000..1aee6a2 --- /dev/null +++ b/internal/kube-handler/client.go @@ -0,0 +1,73 @@ +package kubehandler + +import ( + "context" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + errs "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + errCreateSecret = "create secret failed" + errGetSecret = "get secret failed" + errUpdateFailed = "update secret failed" +) + +// GetSecret retrieves a Kubernetes Secret from the cluster. +func GetSecret(ctx context.Context, kubeClient client.Client, name string, namespace string) (*corev1.Secret, error) { + secret := &corev1.Secret{} + err := kubeClient.Get(ctx, client.ObjectKey{ + Namespace: namespace, + Name: name, + }, secret) + + if err != nil { + return &corev1.Secret{}, errors.Wrap(err, errGetSecret) + } + + return secret, nil +} + +// GetOrCreateSecret retrieves a Kubernetes Secret from the cluster. If the secret does not exist, it creates a new one. +func GetOrCreateSecret(ctx context.Context, kubeClient client.Client, name string, namespace string) (*corev1.Secret, error) { + secret, err := GetSecret(ctx, kubeClient, name, namespace) + if err != nil { + if errs.IsNotFound(err) { + return createSecret(ctx, kubeClient, name, namespace) + } + + return &corev1.Secret{}, err + } + + return secret, nil +} + +// UpdateSecret updates a Kubernetes Secret in the cluster. +func UpdateSecret(ctx context.Context, kubeClient client.Client, secret *corev1.Secret) error { + err := kubeClient.Update(ctx, secret) + if err != nil { + return errors.Wrap(err, errUpdateFailed) + } + + return nil +} + +func createSecret(ctx context.Context, kubeClient client.Client, name string, namespace string) (*corev1.Secret, error) { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + } + + err := kubeClient.Create(ctx, secret) + if err != nil { + return &corev1.Secret{}, errors.Wrap(err, errCreateSecret) + } + + return secret, nil +} diff --git a/internal/kube-handler/client_test.go b/internal/kube-handler/client_test.go new file mode 100644 index 0000000..a20a302 --- /dev/null +++ b/internal/kube-handler/client_test.go @@ -0,0 +1,341 @@ +package kubehandler + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/google/go-cmp/cmp" + errorspkg "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" +) + +var ( + errBoom = errors.New("boom") +) + +func createSpecificSecret(name, namespace, key, value string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: map[string][]byte{ + key: []byte(value), + }, + } +} + +func Test_GetSecret(t *testing.T) { + type args struct { + localKube client.Client + name string + namespace string + } + type want struct { + result *corev1.Secret + err error + } + + cases := map[string]struct { + args args + want want + }{ + "ShouldGetSecret": { + args: args{ + localKube: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + secret, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("object is not a Secret") + } + + *secret = *createSpecificSecret("specific-secret-name", "specific-secret-namespace", "specific-key", "specific-value") + return nil + }, + }, + name: "specific-secret-name", + namespace: "specific-secret-namespace", + }, + want: want{ + result: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "specific-secret-namespace", + Name: "specific-secret-name", + }, + Data: map[string][]uint8{ + "specific-key": []byte("specific-value"), + }, + }, + err: nil, + }, + }, + "ShouldFail": { + args: args{ + localKube: &test.MockClient{ + MockGet: test.NewMockGetFn(errBoom), + }, + name: "secret", + namespace: "default", + }, + want: want{ + result: &corev1.Secret{}, + err: errorspkg.Wrap(errBoom, errGetSecret), + }, + }, + } + for name, tc := range cases { + tc := tc // Create local copies of loop variables + + t.Run(name, func(t *testing.T) { + got, gotErr := GetSecret(context.Background(), tc.args.localKube, tc.args.name, tc.args.namespace) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("GetSecret(...): -want error, +got error: %s", diff) + } + if diff := cmp.Diff(tc.want.result, got); diff != "" { + t.Errorf("GetSecret(...): -want result, +got result: %s", diff) + } + }) + } +} + +func Test_GetOrCreateSecret(t *testing.T) { + type args struct { + localKube client.Client + name string + namespace string + } + type want struct { + result *corev1.Secret + err error + } + + cases := map[string]struct { + args args + want want + }{ + "ShouldGetExistingSecret": { + args: args{ + localKube: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + secret, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("object is not a Secret") + } + + *secret = *createSpecificSecret("specific-secret-name", "specific-secret-namespace", "specific-key", "specific-value") + return nil + }, + }, + name: "specific-secret-name", + namespace: "specific-secret-namespace", + }, + want: want{ + result: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "specific-secret-namespace", + Name: "specific-secret-name", + }, + Data: map[string][]byte{ + "specific-key": []byte("specific-value"), + }, + }, + err: nil, + }, + }, + "ShouldCreateNewSecret": { + args: args{ + localKube: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + secret, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("object is not a Secret") + } + + *secret = *createSpecificSecret("new-secret-name", "new-secret-namespace", "new-key", "new-value") + return nil + }, + MockCreate: func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { + secret, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("object is not a Secret") + } + + *secret = *createSpecificSecret("new-secret-name", "new-secret-namespace", "new-key", "new-value") + return nil + }, + }, + name: "new-secret-name", + namespace: "new-secret-namespace", + }, + want: want{ + result: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "new-secret-namespace", + Name: "new-secret-name", + }, + Data: map[string][]byte{ + "new-key": []byte("new-value"), + }, + }, + err: nil, + }, + }, + + "ShouldFail": { + args: args{ + localKube: &test.MockClient{ + MockGet: test.NewMockGetFn(errBoom), + }, + name: "secret", + namespace: "default", + }, + want: want{ + result: &corev1.Secret{}, + err: errorspkg.Wrap(errBoom, errGetSecret), + }, + }, + } + for name, tc := range cases { + tc := tc // Create local copies of loop variables + + t.Run(name, func(t *testing.T) { + got, gotErr := GetOrCreateSecret(context.Background(), tc.args.localKube, tc.args.name, tc.args.namespace) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("GetOrCreateSecret(...): -want error, +got error: %s", diff) + } + if diff := cmp.Diff(tc.want.result, got); diff != "" { + t.Errorf("GetOrCreateSecret(...): -want result, +got result: %s", diff) + } + }) + } +} + +func Test_UpdateSecret(t *testing.T) { + type args struct { + localKube client.Client + secret *corev1.Secret + } + type want struct { + err error + } + + cases := map[string]struct { + args args + want want + }{ + "ShouldUpdateSecret": { + args: args{ + localKube: &test.MockClient{ + MockUpdate: func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + secret, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("object is not a Secret") + } + + // Simulate updating the secret + secret.Data["updated-key"] = []byte("updated-value") + return nil + }, + }, + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "specific-secret-namespace", + Name: "specific-secret-name", + }, + Data: map[string][]byte{ + "specific-key": []byte("specific-value"), + }, + }, + }, + want: want{ + err: nil, + }, + }, + "ShouldFail": { + args: args{ + localKube: &test.MockClient{ + MockUpdate: test.NewMockUpdateFn(errBoom), + }, + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "specific-secret-namespace", + Name: "specific-secret-name", + }, + Data: map[string][]byte{ + "specific-key": []byte("specific-value"), + }, + }, + }, + want: want{ + err: errorspkg.Wrap(errBoom, errUpdateFailed), + }, + }, + } + for name, tc := range cases { + tc := tc // Create local copies of loop variables + + t.Run(name, func(t *testing.T) { + gotErr := UpdateSecret(context.Background(), tc.args.localKube, tc.args.secret) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("UpdateSecret(...): -want error, +got error: %s", diff) + } + }) + } +} + +func Test_GetSecret_ErrorHandling(t *testing.T) { + // Mock Kubernetes client that always returns an error + kubeClient := &test.MockClient{ + MockGet: test.NewMockGetFn(errBoom), + } + + _, err := GetSecret(context.Background(), kubeClient, "some-secret", "some-namespace") + + // Verify that the error returned is wrapped correctly + if err == nil || !errors.Is(err, errBoom) { + t.Errorf("GetSecret() expected error %v, got: %v", errBoom, err) + } +} + +func Test_GetOrCreateSecret_EmptyName(t *testing.T) { + kubeClient := &test.MockClient{ + MockGet: test.NewMockGetFn(errBoom), + } + // Pass an empty secret name + _, err := GetOrCreateSecret(context.Background(), kubeClient, "", "some-namespace") + + // Verify that an error is returned for an empty secret name + if err == nil || !strings.Contains(err.Error(), errGetSecret) { + t.Errorf("GetOrCreateSecret() with empty name: expected error, got: %v", err) + } +} + +func Test_UpdateSecret_ErrorHandling(t *testing.T) { + // Mock Kubernetes client that always returns an error + kubeClient := &test.MockClient{ + MockUpdate: test.NewMockUpdateFn(errBoom), + } + + // Create a dummy secret for testing + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + Name: "some-secret", + }, + Data: map[string][]byte{ + "key": []byte("value"), + }, + } + + err := UpdateSecret(context.Background(), kubeClient, secret) + + // Verify that the error returned is wrapped correctly + if err == nil || !errors.Is(err, errBoom) { + t.Errorf("UpdateSecret() expected error %v, got: %v", errBoom, err) + } +} diff --git a/internal/utils/set_status_test.go b/internal/utils/set_status_test.go index ad0340d..9020ada 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/v1alpha1" - v1alpha1_request "github.com/crossplane-contrib/provider-http/apis/request/v1alpha1" + v1alpha1_disposable "github.com/crossplane-contrib/provider-http/apis/disposablerequest/v1alpha2" + v1alpha1_request "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/pkg/errors" diff --git a/package/crds/http.crossplane.io_disposablerequests.yaml b/package/crds/http.crossplane.io_disposablerequests.yaml index 648c9f7..a1868ab 100644 --- a/package/crds/http.crossplane.io_disposablerequests.yaml +++ b/package/crds/http.crossplane.io_disposablerequests.yaml @@ -381,6 +381,410 @@ spec: - spec type: object served: true + storage: false + subresources: + status: {} + - 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 an example API type. + 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 ManagementPolicy 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 + 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 for response data. + items: + description: SecretInjectionConfig represents the configuration + for injecting secret data into a Kubernetes secret. + properties: + responsePath: + description: ResponsePath is is a jq filter expression represents + 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 + 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 + required: + - responsePath + - secretKey + - secretRef + type: object + type: array + 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 + managementPolicy: + default: FullControl + description: 'THIS IS AN ALPHA FIELD. Do not use it in production. + It is not honored unless the relevant Crossplane feature flag is + enabled, and may be changed or removed without notice. ManagementPolicy + specifies the level of control Crossplane has over the managed external + resource. 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. + 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: + - FullControl + - ObserveOnly + - OrphanOnDelete + type: string + 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 + providerRef: + description: 'ProviderReference specifies the provider that will be + used to create, observe, update, and delete this managed resource. + Deprecated: Please use ProviderConfigReference, i.e. `providerConfigRef`' + 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 + 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. + 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 + 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 + error: + type: string + failed: + format: int32 + 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.crossplane.io_requests.yaml b/package/crds/http.crossplane.io_requests.yaml index 550a513..5e68293 100644 --- a/package/crds/http.crossplane.io_requests.yaml +++ b/package/crds/http.crossplane.io_requests.yaml @@ -405,6 +405,438 @@ spec: - spec type: object served: true + storage: false + subresources: + status: {} + - 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 an example API type. + 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 ManagementPolicy 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: + 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 + mappings: + description: Mappings defines the HTTP mappings for different + methods. + items: + properties: + body: + type: string + headers: + additionalProperties: + items: + type: string + type: array + type: object + method: + enum: + - POST + - GET + - PUT + - DELETE + type: string + url: + type: string + required: + - method + - url + type: object + type: array + payload: + description: Payload defines the payload for the request. + properties: + baseUrl: + type: string + 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: + responsePath: + description: ResponsePath is is a jq filter expression represents + 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 + 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 + required: + - responsePath + - secretKey + - secretRef + type: object + type: array + waitTimeout: + description: WaitTimeout specifies the maximum time duration for + waiting. + type: string + required: + - mappings + - payload + type: object + managementPolicy: + default: FullControl + description: 'THIS IS AN ALPHA FIELD. Do not use it in production. + It is not honored unless the relevant Crossplane feature flag is + enabled, and may be changed or removed without notice. ManagementPolicy + specifies the level of control Crossplane has over the managed external + resource. 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. + 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: + - FullControl + - ObserveOnly + - OrphanOnDelete + type: string + 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 + providerRef: + description: 'ProviderReference specifies the provider that will be + used to create, observe, update, and delete this managed resource. + Deprecated: Please use ProviderConfigReference, i.e. `providerConfigRef`' + 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 + 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. + 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 + 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 + error: + type: string + failed: + format: int32 + type: integer + requestDetails: + properties: + body: + type: string + headers: + additionalProperties: + items: + type: string + type: array + type: object + method: + enum: + - POST + - GET + - PUT + - DELETE + type: string + url: + type: string + required: + - method + - 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 bf81511..eab1fe0 100644 --- a/package/crossplane.yaml +++ b/package/crossplane.yaml @@ -3,26 +3,20 @@ kind: Provider metadata: name: provider-http annotations: + descriptionShort: | + A Crossplane provider facilitating seamless integration of HTTP interactions. meta.crossplane.io/maintainer: Crossplane Maintainers meta.crossplane.io/source: github.com/crossplane-contrib/provider-http meta.crossplane.io/license: Apache-2.0 + friendly-name.meta.crossplane.io: Provider Http meta.crossplane.io/description: | A Crossplane provider facilitating seamless integration of HTTP interactions. - meta.crossplane.io/readme: | - `provider-http` is a Crossplane Provider designed to leverage HTTP interactions for effective resource management. - - ## Resources - - - **`DisposableRequest` Resource:** - - Enables one-time HTTP interactions. - - - **`Request` Resource:** - - Enables resource management through HTTP requests. - - Explore the available resources and their specifications in the [Docs](https://github.com/crossplane-contrib/provider-http/tree/main/resources-docs). - - ## Get Involved + `provider-http` is a Crossplane Provider designed to leverage HTTP interactions for + effective resource management. Available resources and their fields can be found in the [CRD + Docs](https://doc.crds.dev/github.com/crossplane-contrib/provider-http). - If you have any questions, encounter issues, or want to contribute, 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). + If you have any questions, encounter issues, or want to contribute, + 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). diff --git a/resources-docs/disposablerequest_docs.md b/resources-docs/disposablerequest_docs.md index 34ebb92..cb1fa9e 100644 --- a/resources-docs/disposablerequest_docs.md +++ b/resources-docs/disposablerequest_docs.md @@ -9,7 +9,7 @@ The `DisposableRequest` resource is designed for initiating one-time HTTP reques Here is an example `DisposableRequest` resource definition: ```yaml - apiVersion: http.crossplane.io/v1alpha1 + apiVersion: http.crossplane.io/v1alpha2 kind: DisposableRequest metadata: name: example-disposable-request diff --git a/resources-docs/request_docs.md b/resources-docs/request_docs.md index ddb58ce..1468603 100644 --- a/resources-docs/request_docs.md +++ b/resources-docs/request_docs.md @@ -9,7 +9,7 @@ The `Request` resource is designed for managing a resource through HTTP requests Here is an example `Request` resource definition: ```yaml - apiVersion: http.crossplane.io/v1alpha1 + apiVersion: http.crossplane.io/v1alpha2 kind: Request metadata: name: user-dan @@ -55,7 +55,7 @@ The PUT mapping represents your desired state. The body in this mapping should b Example PUT mapping: ```yaml - apiVersion: http.crossplane.io/v1alpha1 + apiVersion: http.crossplane.io/v1alpha2 ... mappings: ... @@ -106,7 +106,7 @@ Example `Request` status: Here's an example of using variables from the response: ```yaml - apiVersion: http.crossplane.io/v1alpha1 + apiVersion: http.crossplane.io/v1alpha2 kind: Request metadata: name: user-dan