diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 000000000..00502c6b7 --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,34 @@ +name: Backport + +on: + # NOTE(negz): This is a risky target, but we run this action only when and if + # a PR is closed, then filter down to specifically merged PRs. We also don't + # invoke any scripts, etc from within the repo. I believe the fact that we'll + # be able to review PRs before this runs makes this fairly safe. + # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ + pull_request_target: + types: [closed] + # See also commands.yml for the /backport triggered variant of this workflow. + +jobs: + # NOTE(negz): I tested many backport GitHub actions before landing on this + # one. Many do not support merge commits, or do not support pull requests with + # more than one commit. This one does. It also handily links backport PRs with + # new PRs, and provides commentary and instructions when it can't backport. + # The main gotchas with this action are that it _only_ supports merge commits, + # and that PRs _must_ be labelled before they're merged to trigger a backport. + open-pr: + runs-on: ubuntu-18.04 + if: github.event.pull_request.merged + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Open Backport PR + uses: zeebe-io/backport-action@v0.0.4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + github_workspace: ${{ github.workspace }} + version: v0.0.4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 589eee8ca..0db0e7bd2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: env: # Common versions - GO_VERSION: '1.16' + GO_VERSION: '1.17' GOLANGCI_VERSION: 'v1.31' DOCKER_BUILDX_VERSION: 'v0.4.2' diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml new file mode 100644 index 000000000..b9c504c48 --- /dev/null +++ b/.github/workflows/commands.yml @@ -0,0 +1,92 @@ +name: Comment Commands + +on: issue_comment + +jobs: + points: + runs-on: ubuntu-18.04 + if: startsWith(github.event.comment.body, '/points') + + steps: + - name: Extract Command + id: command + uses: xt0rted/slash-command-action@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + command: points + reaction: "true" + reaction-type: "eyes" + allow-edits: "false" + permission-level: write + - name: Handle Command + uses: actions/github-script@v4 + env: + POINTS: ${{ steps.command.outputs.command-arguments }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const points = process.env.POINTS + + if (isNaN(parseInt(points))) { + console.log("Malformed command - expected '/points '") + github.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: "confused" + }) + return + } + const label = "points/" + points + + // Delete our needs-points-label label. + try { + await github.issues.deleteLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: ['needs-points-label'] + }) + console.log("Deleted 'needs-points-label' label.") + } + catch(e) { + console.log("Label 'needs-points-label' probably didn't exist.") + } + + // Add our points label. + github.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: [label] + }) + console.log("Added '" + label + "' label.") + + # NOTE(negz): See also backport.yml, which is the variant that triggers on PR + # merge rather than on comment. + backport: + runs-on: ubuntu-18.04 + if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/backport') + steps: + - name: Extract Command + id: command + uses: xt0rted/slash-command-action@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + command: backport + reaction: "true" + reaction-type: "eyes" + allow-edits: "false" + permission-level: write + + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Open Backport PR + uses: zeebe-io/backport-action@v0.0.4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + github_workspace: ${{ github.workspace }} + version: v0.0.4 diff --git a/Makefile b/Makefile index d348aa7a6..941a62b3c 100644 --- a/Makefile +++ b/Makefile @@ -57,16 +57,6 @@ cobertura: grep -v zz_generated.deepcopy | \ $(GOCOVER_COBERTURA) > $(GO_TEST_OUTPUT)/cobertura-coverage.xml -# Ensure a PR is ready for review. -reviewable: generate lint - @go mod tidy - -# Ensure branch is clean. -check-diff: reviewable - @$(INFO) checking that branch is clean - @git diff --quiet || $(FAIL) - @$(OK) branch is clean - # Update the submodules, such as the common build scripts. submodules: @git submodule sync diff --git a/OWNERS.md b/OWNERS.md index 7a8bb3580..cc45e943c 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -12,4 +12,5 @@ guidelines and responsibilities for the steering committee and maintainers. * Nic Cope ([negz](https://github.com/negz)) * Daniel Mangum ([hasheddan](https://github.com/hasheddan)) -* Muvaffak Onus ([muvaf](https://github.com/muvaf)) +* Muvaffak Onuş ([muvaf](https://github.com/muvaf)) +* Hasan Türken ([turkenh](https://github.com/turkenh)) diff --git a/apis/common/v1/condition_test.go b/apis/common/v1/condition_test.go index 5b9ab06dc..12ec04616 100644 --- a/apis/common/v1/condition_test.go +++ b/apis/common/v1/condition_test.go @@ -20,9 +20,10 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/crossplane/crossplane-runtime/pkg/errors" ) func TestConditionEqual(t *testing.T) { diff --git a/apis/common/v1/connection_details.go b/apis/common/v1/connection_details.go new file mode 100644 index 000000000..9b5e871fe --- /dev/null +++ b/apis/common/v1/connection_details.go @@ -0,0 +1,226 @@ +/* +Copyright 2019 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 v1 + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" +) + +const ( + // LabelKeyOwnerUID is the UID of the owner resource of a connection secret. + // Kubernetes provides owner/controller references to track ownership of + // resources including secrets, however, this would only work for in cluster + // k8s secrets. We opted to use a label for this purpose to be consistent + // across Secret Store implementations and expect all to support + // setting/getting labels. + LabelKeyOwnerUID = "secret.crossplane.io/owner-uid" +) + +// PublishConnectionDetailsTo represents configuration of a connection secret. +type PublishConnectionDetailsTo struct { + // Name is the name of the connection secret. + Name string `json:"name"` + + // Metadata is the metadata for connection secret. + // +optional + Metadata *ConnectionSecretMetadata `json:"metadata,omitempty"` + + // SecretStoreConfigRef specifies which secret store config should be used + // for this ConnectionSecret. + // +optional + // +kubebuilder:default={"name": "default"} + SecretStoreConfigRef *Reference `json:"configRef,omitempty"` +} + +// ConnectionSecretMetadata represents metadata of a connection secret. +// Labels are used to track ownership of connection secrets and has to be +// supported for any secret store implementation. +type ConnectionSecretMetadata struct { + // 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. + // +optional + Labels map[string]string `json:"labels,omitempty"` + // 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. + // +optional + Annotations map[string]string `json:"annotations,omitempty"` + // Type is the SecretType for the connection secret. + // - Only valid for Kubernetes Secret Stores. + // +optional + Type *corev1.SecretType `json:"type,omitempty"` +} + +// SetOwnerUID sets owner object uid label. +func (in *ConnectionSecretMetadata) SetOwnerUID(uid types.UID) { + if in.Labels == nil { + in.Labels = map[string]string{} + } + in.Labels[LabelKeyOwnerUID] = string(uid) +} + +// GetOwnerUID gets owner object uid. +func (in *ConnectionSecretMetadata) GetOwnerUID() string { + if u, ok := in.Labels[LabelKeyOwnerUID]; ok { + return u + } + return "" +} + +// SecretStoreType represents a secret store type. +type SecretStoreType string + +const ( + // SecretStoreKubernetes indicates that secret store type is + // Kubernetes. In other words, connection secrets will be stored as K8s + // Secrets. + SecretStoreKubernetes SecretStoreType = "Kubernetes" + + // SecretStoreVault indicates that secret store type is Vault. + SecretStoreVault SecretStoreType = "Vault" +) + +// SecretStoreConfig represents configuration of a Secret Store. +type SecretStoreConfig struct { + // Type configures which secret store to be used. Only the configuration + // block for this store will be used and others will be ignored if provided. + // Default is Kubernetes. + // +optional + // +kubebuilder:default=Kubernetes + Type *SecretStoreType `json:"type,omitempty"` + + // DefaultScope used for scoping secrets for "cluster-scoped" resources. + // If store type is "Kubernetes", this would mean the default namespace to + // store connection secrets for cluster scoped resources. + // In case of "Vault", this would be used as the default parent path. + // Typically, should be set as Crossplane installation namespace. + DefaultScope string `json:"defaultScope"` + + // Kubernetes configures a Kubernetes secret store. + // If the "type" is "Kubernetes" but no config provided, in cluster config + // will be used. + // +optional + Kubernetes *KubernetesSecretStoreConfig `json:"kubernetes,omitempty"` + + // Vault configures a Vault secret store. + // +optional + Vault *VaultSecretStoreConfig `json:"vault,omitempty"` +} + +// KubernetesAuthConfig required to authenticate to a K8s API. It expects +// a "kubeconfig" file to be provided. +type KubernetesAuthConfig struct { + // Source of the credentials. + // +kubebuilder:validation:Enum=None;Secret;Environment;Filesystem + Source CredentialsSource `json:"source"` + + // CommonCredentialSelectors provides common selectors for extracting + // credentials. + CommonCredentialSelectors `json:",inline"` +} + +// KubernetesSecretStoreConfig represents the required configuration +// for a Kubernetes secret store. +type KubernetesSecretStoreConfig struct { + // Credentials used to connect to the Kubernetes API. + Auth KubernetesAuthConfig `json:"auth"` + + // TODO(turkenh): Support additional identities like + // https://github.com/crossplane-contrib/provider-kubernetes/blob/4d722ef914e6964e80e190317daca9872ae98738/apis/v1alpha1/types.go#L34 +} + +// VaultAuthMethod represent a Vault authentication method. +// https://www.vaultproject.io/docs/auth +type VaultAuthMethod string + +const ( + // VaultAuthToken indicates that "Token Auth" will be used to + // authenticate to Vault. + // https://www.vaultproject.io/docs/auth/token + VaultAuthToken VaultAuthMethod = "Token" +) + +// VaultAuthTokenConfig represents configuration for Vault Token Auth Method. +// https://www.vaultproject.io/docs/auth/token +type VaultAuthTokenConfig struct { + // Source of the credentials. + // +kubebuilder:validation:Enum=None;Secret;Environment;Filesystem + Source CredentialsSource `json:"source"` + + // CommonCredentialSelectors provides common selectors for extracting + // credentials. + CommonCredentialSelectors `json:",inline"` +} + +// VaultAuthConfig required to authenticate to a Vault API. +type VaultAuthConfig struct { + // Method configures which auth method will be used. + Method VaultAuthMethod `json:"method"` + // Token configures Token Auth for Vault. + // +optional + Token *VaultAuthTokenConfig `json:"token,omitempty"` +} + +// VaultCABundleConfig represents configuration for configuring a CA bundle. +type VaultCABundleConfig struct { + // Source of the credentials. + // +kubebuilder:validation:Enum=None;Secret;Environment;Filesystem + Source CredentialsSource `json:"source"` + + // CommonCredentialSelectors provides common selectors for extracting + // credentials. + CommonCredentialSelectors `json:",inline"` +} + +// VaultKVVersion represent API version of the Vault KV engine +// https://www.vaultproject.io/docs/secrets/kv +type VaultKVVersion string + +const ( + // VaultKVVersionV1 indicates that Secret API is KV Secrets Engine Version 1 + // https://www.vaultproject.io/docs/secrets/kv/kv-v1 + VaultKVVersionV1 VaultKVVersion = "v1" + + // VaultKVVersionV2 indicates that Secret API is KV Secrets Engine Version 2 + // https://www.vaultproject.io/docs/secrets/kv/kv-v2 + VaultKVVersionV2 VaultKVVersion = "v2" +) + +// VaultSecretStoreConfig represents the required configuration for a Vault +// secret store. +type VaultSecretStoreConfig struct { + // Server is the url of the Vault server, e.g. "https://vault.acme.org" + Server string `json:"server"` + + // MountPath is the mount path of the KV secrets engine. + MountPath string `json:"mountPath"` + + // Version of the KV Secrets engine of Vault. + // https://www.vaultproject.io/docs/secrets/kv + // +optional + // +kubebuilder:default=v2 + Version *VaultKVVersion `json:"version,omitempty"` + + // CABundle configures CA bundle for Vault Server. + // +optional + CABundle *VaultCABundleConfig `json:"caBundle,omitempty"` + + // Auth configures an authentication method for Vault. + Auth VaultAuthConfig `json:"auth"` +} diff --git a/apis/common/v1/policies.go b/apis/common/v1/policies.go index 81b74814f..ca0fa7d90 100644 --- a/apis/common/v1/policies.go +++ b/apis/common/v1/policies.go @@ -45,3 +45,26 @@ const ( // update. UpdateManual UpdatePolicy = "Manual" ) + +// ResolvePolicy is a type for resolve policy. +type ResolvePolicy string + +// ResolutionPolicy is a type for resolution policy. +type ResolutionPolicy string + +const ( + // ResolvePolicyAlways is a resolve option. + // When the ResolvePolicy is set to ResolvePolicyAlways the reference will + // be tried to resolve for every reconcile loop. + ResolvePolicyAlways ResolvePolicy = "Always" + + // ResolutionPolicyRequired is a resolution option. + // When the ResolutionPolicy is set to ResolutionPolicyRequired the execution + // could not continue even if the reference cannot be resolved. + ResolutionPolicyRequired ResolutionPolicy = "Required" + + // ResolutionPolicyOptional is a resolution option. + // When the ReferenceResolutionPolicy is set to ReferencePolicyOptional the + // execution could continue even if the reference cannot be resolved. + ResolutionPolicyOptional ResolutionPolicy = "Optional" +) diff --git a/apis/common/v1/resource.go b/apis/common/v1/resource.go index 15bc7b630..872d9a098 100644 --- a/apis/common/v1/resource.go +++ b/apis/common/v1/resource.go @@ -77,10 +77,50 @@ type SecretKeySelector struct { Key string `json:"key"` } +// Policy represents the Resolve and Resolution policies of Reference instance. +type Policy struct { + // 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. + // +optional + // +kubebuilder:validation:Enum=Always;IfNotPresent + Resolve *ResolvePolicy `json:"resolve,omitempty"` + + // 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. + // +optional + // +kubebuilder:default=Required + // +kubebuilder:validation:Enum=Required;Optional + Resolution *ResolutionPolicy `json:"resolution,omitempty"` +} + +// IsResolutionPolicyOptional checks whether the resolution policy of relevant reference is Optional. +func (p *Policy) IsResolutionPolicyOptional() bool { + if p == nil || p.Resolution == nil { + return false + } + return *p.Resolution == ResolutionPolicyOptional +} + +// IsResolvePolicyAlways checks whether the resolution policy of relevant reference is Always. +func (p *Policy) IsResolvePolicyAlways() bool { + if p == nil || p.Resolve == nil { + return false + } + return *p.Resolve == ResolvePolicyAlways +} + // A Reference to a named object. type Reference struct { // Name of the referenced object. Name string `json:"name"` + + // Policies for referencing. + // +optional + Policy *Policy `json:"policy,omitempty"` } // A TypedReference refers to an object by Name, Kind, and APIVersion. It is @@ -109,6 +149,10 @@ type Selector struct { // MatchControllerRef ensures an object with the same controller reference // as the selecting object is selected. MatchControllerRef *bool `json:"matchControllerRef,omitempty"` + + // Policies for selection. + // +optional + Policy *Policy `json:"policy,omitempty"` } // SetGroupVersionKind sets the Kind and APIVersion of a TypedReference. @@ -133,9 +177,21 @@ type ResourceSpec struct { // 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. // +optional WriteConnectionSecretToReference *SecretReference `json:"writeConnectionSecretToRef,omitempty"` + // 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. + // +optional + PublishConnectionDetailsTo *PublishConnectionDetailsTo `json:"publishConnectionDetailsTo,omitempty"` + // ProviderConfigReference specifies how the provider that will be used to // create, observe, update, and delete this managed resource should be // configured. @@ -239,6 +295,8 @@ type ProviderConfigUsage struct { // A TargetSpec defines the common fields of objects used for exposing // infrastructure to workloads that can be scheduled to. +// +// Deprecated. type TargetSpec struct { // WriteConnectionSecretToReference specifies the name of a Secret, in the // same namespace as this target, to which any connection details for this @@ -256,6 +314,8 @@ type TargetSpec struct { } // A TargetStatus defines the observed status a target. +// +// Deprecated. type TargetStatus struct { ConditionedStatus `json:",inline"` } diff --git a/apis/common/v1/zz_generated.deepcopy.go b/apis/common/v1/zz_generated.deepcopy.go index 3ae4bb9a5..5f8a7e345 100644 --- a/apis/common/v1/zz_generated.deepcopy.go +++ b/apis/common/v1/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* @@ -92,6 +93,40 @@ func (in *ConditionedStatus) DeepCopy() *ConditionedStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectionSecretMetadata) DeepCopyInto(out *ConnectionSecretMetadata) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(corev1.SecretType) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionSecretMetadata. +func (in *ConnectionSecretMetadata) DeepCopy() *ConnectionSecretMetadata { + if in == nil { + return nil + } + out := new(ConnectionSecretMetadata) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnvSelector) DeepCopyInto(out *EnvSelector) { *out = *in @@ -122,6 +157,38 @@ func (in *FsSelector) DeepCopy() *FsSelector { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubernetesAuthConfig) DeepCopyInto(out *KubernetesAuthConfig) { + *out = *in + in.CommonCredentialSelectors.DeepCopyInto(&out.CommonCredentialSelectors) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesAuthConfig. +func (in *KubernetesAuthConfig) DeepCopy() *KubernetesAuthConfig { + if in == nil { + return nil + } + out := new(KubernetesAuthConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubernetesSecretStoreConfig) DeepCopyInto(out *KubernetesSecretStoreConfig) { + *out = *in + in.Auth.DeepCopyInto(&out.Auth) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesSecretStoreConfig. +func (in *KubernetesSecretStoreConfig) DeepCopy() *KubernetesSecretStoreConfig { + if in == nil { + return nil + } + out := new(KubernetesSecretStoreConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LocalSecretReference) DeepCopyInto(out *LocalSecretReference) { *out = *in @@ -162,6 +229,31 @@ func (in *MergeOptions) DeepCopy() *MergeOptions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Policy) DeepCopyInto(out *Policy) { + *out = *in + if in.Resolve != nil { + in, out := &in.Resolve, &out.Resolve + *out = new(ResolvePolicy) + **out = **in + } + if in.Resolution != nil { + in, out := &in.Resolution, &out.Resolution + *out = new(ResolutionPolicy) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Policy. +func (in *Policy) DeepCopy() *Policy { + if in == nil { + return nil + } + out := new(Policy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProviderConfigStatus) DeepCopyInto(out *ProviderConfigStatus) { *out = *in @@ -181,7 +273,7 @@ func (in *ProviderConfigStatus) DeepCopy() *ProviderConfigStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProviderConfigUsage) DeepCopyInto(out *ProviderConfigUsage) { *out = *in - out.ProviderConfigReference = in.ProviderConfigReference + in.ProviderConfigReference.DeepCopyInto(&out.ProviderConfigReference) out.ResourceReference = in.ResourceReference } @@ -195,9 +287,39 @@ func (in *ProviderConfigUsage) DeepCopy() *ProviderConfigUsage { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PublishConnectionDetailsTo) DeepCopyInto(out *PublishConnectionDetailsTo) { + *out = *in + if in.Metadata != nil { + in, out := &in.Metadata, &out.Metadata + *out = new(ConnectionSecretMetadata) + (*in).DeepCopyInto(*out) + } + if in.SecretStoreConfigRef != nil { + in, out := &in.SecretStoreConfigRef, &out.SecretStoreConfigRef + *out = new(Reference) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublishConnectionDetailsTo. +func (in *PublishConnectionDetailsTo) DeepCopy() *PublishConnectionDetailsTo { + if in == nil { + return nil + } + out := new(PublishConnectionDetailsTo) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Reference) DeepCopyInto(out *Reference) { *out = *in + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(Policy) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Reference. @@ -218,15 +340,20 @@ func (in *ResourceSpec) DeepCopyInto(out *ResourceSpec) { *out = new(SecretReference) **out = **in } + if in.PublishConnectionDetailsTo != nil { + in, out := &in.PublishConnectionDetailsTo, &out.PublishConnectionDetailsTo + *out = new(PublishConnectionDetailsTo) + (*in).DeepCopyInto(*out) + } if in.ProviderConfigReference != nil { in, out := &in.ProviderConfigReference, &out.ProviderConfigReference *out = new(Reference) - **out = **in + (*in).DeepCopyInto(*out) } if in.ProviderReference != nil { in, out := &in.ProviderReference, &out.ProviderReference *out = new(Reference) - **out = **in + (*in).DeepCopyInto(*out) } } @@ -287,6 +414,36 @@ func (in *SecretReference) DeepCopy() *SecretReference { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretStoreConfig) DeepCopyInto(out *SecretStoreConfig) { + *out = *in + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(SecretStoreType) + **out = **in + } + if in.Kubernetes != nil { + in, out := &in.Kubernetes, &out.Kubernetes + *out = new(KubernetesSecretStoreConfig) + (*in).DeepCopyInto(*out) + } + if in.Vault != nil { + in, out := &in.Vault, &out.Vault + *out = new(VaultSecretStoreConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreConfig. +func (in *SecretStoreConfig) DeepCopy() *SecretStoreConfig { + if in == nil { + return nil + } + out := new(SecretStoreConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Selector) DeepCopyInto(out *Selector) { *out = *in @@ -302,6 +459,11 @@ func (in *Selector) DeepCopyInto(out *Selector) { *out = new(bool) **out = **in } + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(Policy) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Selector. @@ -369,3 +531,81 @@ func (in *TypedReference) DeepCopy() *TypedReference { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VaultAuthConfig) DeepCopyInto(out *VaultAuthConfig) { + *out = *in + if in.Token != nil { + in, out := &in.Token, &out.Token + *out = new(VaultAuthTokenConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultAuthConfig. +func (in *VaultAuthConfig) DeepCopy() *VaultAuthConfig { + if in == nil { + return nil + } + out := new(VaultAuthConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VaultAuthTokenConfig) DeepCopyInto(out *VaultAuthTokenConfig) { + *out = *in + in.CommonCredentialSelectors.DeepCopyInto(&out.CommonCredentialSelectors) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultAuthTokenConfig. +func (in *VaultAuthTokenConfig) DeepCopy() *VaultAuthTokenConfig { + if in == nil { + return nil + } + out := new(VaultAuthTokenConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VaultCABundleConfig) DeepCopyInto(out *VaultCABundleConfig) { + *out = *in + in.CommonCredentialSelectors.DeepCopyInto(&out.CommonCredentialSelectors) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultCABundleConfig. +func (in *VaultCABundleConfig) DeepCopy() *VaultCABundleConfig { + if in == nil { + return nil + } + out := new(VaultCABundleConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VaultSecretStoreConfig) DeepCopyInto(out *VaultSecretStoreConfig) { + *out = *in + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(VaultKVVersion) + **out = **in + } + if in.CABundle != nil { + in, out := &in.CABundle, &out.CABundle + *out = new(VaultCABundleConfig) + (*in).DeepCopyInto(*out) + } + in.Auth.DeepCopyInto(&out.Auth) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultSecretStoreConfig. +func (in *VaultSecretStoreConfig) DeepCopy() *VaultSecretStoreConfig { + if in == nil { + return nil + } + out := new(VaultSecretStoreConfig) + in.DeepCopyInto(out) + return out +} diff --git a/build b/build index a6cd5ea19..bd63a4167 160000 --- a/build +++ b/build @@ -1 +1 @@ -Subproject commit a6cd5ea19f2b85e724656eb0f1e6a2f810069c1a +Subproject commit bd63a4167ae20a71788537217b022fced8f2f854 diff --git a/go.mod b/go.mod index dc940b7c0..5b300f237 100644 --- a/go.mod +++ b/go.mod @@ -1,20 +1,124 @@ module github.com/crossplane/crossplane-runtime -go 1.13 +go 1.17 require ( - github.com/go-logr/logr v0.4.0 - github.com/google/go-cmp v0.5.5 + github.com/go-logr/logr v1.2.0 + github.com/google/go-cmp v0.5.6 github.com/hashicorp/go-getter v1.4.0 + github.com/hashicorp/vault/api v1.3.1 github.com/imdario/mergo v0.3.12 - github.com/pkg/errors v0.9.1 - github.com/spf13/afero v1.2.2 - golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 - k8s.io/api v0.21.2 - k8s.io/apiextensions-apiserver v0.21.2 - k8s.io/apimachinery v0.21.2 - k8s.io/client-go v0.21.2 - sigs.k8s.io/controller-runtime v0.9.2 - sigs.k8s.io/controller-tools v0.2.4 - sigs.k8s.io/yaml v1.2.0 + github.com/spf13/afero v1.8.0 + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac + k8s.io/api v0.23.0 + k8s.io/apiextensions-apiserver v0.23.0 + k8s.io/apimachinery v0.23.0 + k8s.io/client-go v0.23.0 + sigs.k8s.io/controller-runtime v0.11.0 + sigs.k8s.io/controller-tools v0.8.0 + sigs.k8s.io/yaml v1.3.0 +) + +require ( + cloud.google.com/go v0.81.0 // indirect + cloud.google.com/go/storage v1.14.0 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.18 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/logger v0.2.1 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/armon/go-metrics v0.3.9 // indirect + github.com/armon/go-radix v1.0.0 // indirect + github.com/aws/aws-sdk-go v1.15.78 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect + github.com/cenkalti/backoff/v3 v3.0.0 // indirect + github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/fatih/color v1.12.0 // indirect + github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect + github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/gobuffalo/flect v0.2.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/googleapis/gax-go/v2 v2.0.5 // indirect + github.com/googleapis/gnostic v0.5.5 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v0.16.2 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.4.3 // indirect + github.com/hashicorp/go-retryablehttp v0.6.6 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-safetemp v1.0.0 // indirect + github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 // indirect + github.com/hashicorp/go-sockaddr v1.0.2 // indirect + github.com/hashicorp/go-uuid v1.0.2 // indirect + github.com/hashicorp/go-version v1.2.0 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/vault/sdk v0.3.0 // indirect + github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/jstemmer/go-junit-report v0.9.1 // indirect + github.com/mattn/go-colorable v0.1.8 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.4.2 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/oklog/run v1.0.0 // indirect + github.com/pierrec/lz4 v2.5.2+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.11.0 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.28.0 // indirect + github.com/prometheus/procfs v0.6.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/spf13/cobra v1.2.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/ulikunitz/xz v0.5.5 // indirect + go.opencensus.io v0.23.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect + golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect + golang.org/x/mod v0.4.2 // indirect + golang.org/x/net v0.0.0-20210825183410-e898025ed96a // indirect + golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect + golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 // indirect + golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect + google.golang.org/api v0.44.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2 // indirect + google.golang.org/grpc v1.41.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/square/go-jose.v2 v2.5.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + k8s.io/component-base v0.23.0 // indirect + k8s.io/klog/v2 v2.30.0 // indirect + k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect + k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect + sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect ) diff --git a/go.sum b/go.sum index c5f0b57f1..c1f857a8a 100644 --- a/go.sum +++ b/go.sum @@ -3,76 +3,88 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0 h1:3ithwDMr7/3vpAMXiH+ZQnYbuIsh+OPhUPMFC9enmn0= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0 h1:xE3CPsOgttP4ACBePh79zTKALtXwn/Edhcr16R5hMWU= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0 h1:Lpy6hKgdcl7a3WGSfJIFmxmcdjSpP6OmBEfcOv1Y680= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0 h1:UDpwYIwla4jHGzZJaEJYx1tOejbgSoNqsAfHAUYe2r8= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0 h1:6RRlFMv1omScs6iq2hfE3IvgE+l6RfJPampq8UZc5TU= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= -github.com/Azure/go-autorest/autorest v0.11.12 h1:gI8ytXbxMfI+IVbI9mP2JGCTXIuhHLgRlvQ9X4PsnHE= -github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= -github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= -github.com/Azure/go-autorest/autorest/adal v0.9.5 h1:Y3bBUV4rTuxenJJs41HU3qmqsb+auo+a3Lz+PlJPpL0= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest v0.11.18 h1:90Y4srNYrwOtAgVo3ndrQkTYn6kf1Eg/AjTFJ8Is2aM= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= -github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8Pcx+3oqrE= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.9 h1:O2sNqxBdvq8Eq5xmzljcYzAORli6RWCvEym4cJf9m18= +github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws/aws-sdk-go v1.15.78 h1:LaXy6lWR0YK7LKyuU0QWy2ws/LWTPfYV/UgfiBu4tvY= github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= +github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -81,9 +93,13 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1U github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= +github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= @@ -92,143 +108,126 @@ github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXH github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= +github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/etcd v3.3.15+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= -github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs= -github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= +github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= +github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= +github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= +github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= -github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= -github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/zapr v0.4.0 h1:uc1uML3hRYL9/ZZPdgHS/n8Nzo+eaYL/Efxkkamf7OM= -github.com/go-logr/zapr v0.4.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= -github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= -github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= -github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= -github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= -github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= -github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= -github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= -github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= -github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= -github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/zapr v1.2.0 h1:n4JnPI1T3Qq1SFEi/F8rwLrZERp2bso19PJZDB9dayk= +github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= -github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= -github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= -github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= -github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= -github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= -github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= -github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= -github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= -github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= -github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= -github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= -github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= -github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= -github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= -github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= -github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= -github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= -github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/gobuffalo/flect v0.1.5 h1:xpKq9ap8MbYfhuPCF0dBH854Gp9CxZjr/IocxELFflo= -github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= +github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= +github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gobuffalo/flect v0.2.3 h1:f/ZukRnSNA/DUpSNDadko7Qc0PhGvsew35p/2tu+CRY= +github.com/gobuffalo/flect v0.2.3/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -238,103 +237,161 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/cel-go v0.9.0/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= +github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= -github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.3.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-getter v1.4.0 h1:ENHNi8494porjD0ZhIrjlAHnveSFhY7hvOJrV/fsKkw= github.com/hashicorp/go-getter v1.4.0/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= +github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= +github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= +github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 h1:78ki3QBevHwYrVxnyVeaEz+7WtifHhauYF23es/0KlI= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 h1:nd0HIW15E6FG1MsnArYaHfuw9C2zgzM8LxkG5Ty/788= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= +github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/vault/api v1.3.1 h1:pkDkcgTh47PRjY1NEFeofqR4W/HkNUi9qIakESO2aRM= +github.com/hashicorp/vault/api v1.3.1/go.mod h1:QeJoWxMFt+MsuWcYhmwRLwKEXrjwAFFywzhptMsTIUw= +github.com/hashicorp/vault/sdk v0.3.0 h1:kR3dpxNkhh/wr6ycaJYqp6AFT/i2xaftbfnwZduTKEY= +github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -347,54 +404,65 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -405,40 +473,44 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= -github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= +github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= +github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= @@ -448,59 +520,62 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.28.0 h1:vGVfV9KrDTvWt5boZO0I19g2E3CsWfpPPKZM9dt3mEw= +github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.8.0 h1:5MmtuhAgYeU6qpa7w7bP0dv6MBYuup0vekhSpSkoq60= +github.com/spf13/afero v1.8.0/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= -github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -509,59 +584,79 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok= github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= +go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= +go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= +go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= +go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= @@ -581,8 +676,10 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -591,13 +688,14 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJtU78+lbEenGbgqB5hfHCQ= -golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -605,16 +703,14 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -623,28 +719,55 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210825183410-e898025ed96a h1:bRuuGXV8wwSdGTB+CtJf+FjgO1APK1CoO39T4BN/XBw= +golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -652,74 +775,94 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 h1:M69LAlWZCshgp0QSzyDcSsSIejIEeuaCVpmwcKwyLMk= +golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 h1:Vv0JUPWTyeqUq42B2WJ1FeIDjjvGKoA2Ss+Ts0lAVbs= -golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -729,7 +872,6 @@ golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -752,13 +894,32 @@ golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff h1:VX/uD7MK0AHXGiScH3fsieUQUcpmRERPDYtqZdJnA+Q= +golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -766,9 +927,6 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= -gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -778,15 +936,29 @@ google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0 h1:URs6qR1lAxDsqWITsQXI4ZkGiYJ5dHtRNiCpfs2OeKA= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -804,19 +976,62 @@ google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2 h1:NHN4wOCScVzKhPenJ2dt+BTs3X/XkBVI/Rh4iDt55T8= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.41.0 h1:f+PlOh7QV4iIJkPrx5NQ7qaNGFQ3OTse67yaDHfju4E= +google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -828,42 +1043,43 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -871,65 +1087,44 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.0.0-20190918155943-95b840bb6a1f/go.mod h1:uWuOHnjmNrtQomJrvEBg0c0HRNyQ+8KTEERVsK0PW48= -k8s.io/api v0.21.2 h1:vz7DqmRsXTCSa6pNxXwQ1IYeAZgdIsua+DZU+o+SX3Y= -k8s.io/api v0.21.2/go.mod h1:Lv6UGJZ1rlMI1qusN8ruAp9PUBFyBwpEHAdG24vIsiU= -k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783/go.mod h1:xvae1SZB3E17UpV59AWc271W/Ph25N+bjPyR63X6tPY= -k8s.io/apiextensions-apiserver v0.21.2 h1:+exKMRep4pDrphEafRvpEi79wTnCFMqKf8LBtlA3yrE= -k8s.io/apiextensions-apiserver v0.21.2/go.mod h1:+Axoz5/l3AYpGLlhJDfcVQzCerVYq3K3CvDMvw6X1RA= -k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4= -k8s.io/apimachinery v0.21.2 h1:vezUc/BHqWlQDnZ+XkrpXSmnANSLbpnlpwo0Lhk0gpc= -k8s.io/apimachinery v0.21.2/go.mod h1:CdTY8fU/BlvAbJ2z/8kBwimGki5Zp8/fbVuLY8gJumM= -k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad/go.mod h1:XPCXEwhjaFN29a8NldXA901ElnKeKLrLtREO9ZhFyhg= -k8s.io/apiserver v0.21.2/go.mod h1:lN4yBoGyiNT7SC1dmNk0ue6a5Wi6O3SWOIw91TsucQw= -k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90/go.mod h1:J69/JveO6XESwVgG53q3Uz5OSfgsv4uxpScmmyYOOlk= -k8s.io/client-go v0.21.2 h1:Q1j4L/iMN4pTw6Y4DWppBoUxgKO8LbffEMVEV00MUp0= -k8s.io/client-go v0.21.2/go.mod h1:HdJ9iknWpbl3vMGtib6T2PyI/VYxiZfq936WNVHBRrA= -k8s.io/code-generator v0.0.0-20190912054826-cd179ad6a269/go.mod h1:V5BD6M4CyaN5m+VthcclXWsVcT1Hu+glwa1bi3MIsyE= -k8s.io/code-generator v0.21.2/go.mod h1:8mXJDCB7HcRo1xiEQstcguZkbxZaqeUOrO9SsicWs3U= -k8s.io/component-base v0.0.0-20190918160511-547f6c5d7090/go.mod h1:933PBGtQFJky3TEwYx4aEPZ4IxqhWh3R6DCmzqIn1hA= -k8s.io/component-base v0.21.2 h1:EsnmFFoJ86cEywC0DoIkAUiEV6fjgauNugiw1lmIjs4= -k8s.io/component-base v0.21.2/go.mod h1:9lvmIThzdlrJj5Hp8Z/TOgIkdfsNARQ1pT+3PByuiuc= -k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ= -k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.23.0 h1:WrL1gb73VSC8obi8cuYETJGXEoFNEh3LU0Pt+Sokgro= +k8s.io/api v0.23.0/go.mod h1:8wmDdLBHBNxtOIytwLstXt5E9PddnZb0GaMcqsvDBpg= +k8s.io/apiextensions-apiserver v0.23.0 h1:uii8BYmHYiT2ZTAJxmvc3X8UhNYMxl2A0z0Xq3Pm+WY= +k8s.io/apiextensions-apiserver v0.23.0/go.mod h1:xIFAEEDlAZgpVBl/1VSjGDmLoXAWRG40+GsWhKhAxY4= +k8s.io/apimachinery v0.23.0 h1:mIfWRMjBuMdolAWJ3Fd+aPTMv3X9z+waiARMpvvb0HQ= +k8s.io/apimachinery v0.23.0/go.mod h1:fFCTTBKvKcwTPFzjlcxp91uPFZr+JA0FubU4fLzzFYc= +k8s.io/apiserver v0.23.0/go.mod h1:Cec35u/9zAepDPPFyT+UMrgqOCjgJ5qtfVJDxjZYmt4= +k8s.io/client-go v0.23.0 h1:vcsOqyPq7XV3QmQRCBH/t9BICJM9Q1M18qahjv+rebY= +k8s.io/client-go v0.23.0/go.mod h1:hrDnpnK1mSr65lHHcUuIZIXDgEbzc7/683c6hyG4jTA= +k8s.io/code-generator v0.23.0/go.mod h1:vQvOhDXhuzqiVfM/YHp+dmg10WDZCchJVObc9MvowsE= +k8s.io/component-base v0.23.0 h1:UAnyzjvVZ2ZR1lF35YwtNY6VMN94WtOnArcXBu34es8= +k8s.io/component-base v0.23.0/go.mod h1:DHH5uiFvLC1edCpvcTDV++NKULdYYU6pR9Tt3HIKMKI= +k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.8.0 h1:Q3gmuM9hKEjefWFFYF0Mat+YyFJvsUyYuwyNNJ5C9Ts= -k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= -k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7 h1:vEx13qjvaZ4yfObSSXW7BrMc/KQBBT/Jyee8XtLf4x0= -k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= -k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210527160623-6fdb442a123b h1:MSqsVQ3pZvPGTqCjptfimO2WjG7A9un2zcpiHkA6M/s= -k8s.io/utils v0.0.0-20210527160623-6fdb442a123b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= -modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= -modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= -modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= -modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= +k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= +k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= +k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b h1:wxEMGetGMur3J1xuGLQY7GEQYg9bZxKn3tKo5k/eYcs= +k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.19/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/controller-runtime v0.9.2 h1:MnCAsopQno6+hI9SgJHKddzXpmv2wtouZz6931Eax+Q= -sigs.k8s.io/controller-runtime v0.9.2/go.mod h1:TxzMCHyEUpaeuOiZx/bIdc2T81vfs/aKdvJt9wuu0zk= -sigs.k8s.io/controller-tools v0.2.4 h1:la1h46EzElvWefWLqfsXrnsO3lZjpkI0asTpX6h8PLA= -sigs.k8s.io/controller-tools v0.2.4/go.mod h1:m/ztfQNocGYBgTTCmFdnK94uVvgxeZeE3LtJvd/jIzA= -sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= -sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca h1:6dsH6AYQWbyZmtttJNe8Gq1cXOeS1BdV3eW37zHilAQ= -sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.25/go.mod h1:Mlj9PNLmG9bZ6BHFwFKDo5afkpWyUISkb9Me0GnK66I= +sigs.k8s.io/controller-runtime v0.11.0 h1:DqO+c8mywcZLFJWILq4iktoECTyn30Bkj0CwgqMpZWQ= +sigs.k8s.io/controller-runtime v0.11.0/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA= +sigs.k8s.io/controller-tools v0.8.0 h1:uUkfTGEwrguqYYfcI2RRGUnC8mYdCFDqfwPKUcNJh1o= +sigs.k8s.io/controller-tools v0.8.0/go.mod h1:qE2DXhVOiEq5ijmINcFbqi9GZrrUjzB1TuJU0xa6eoY= +sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s= +sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.0 h1:C4r9BgJ98vrKnnVCjwCSXcWjWe0NKcUQkmzDXZXGwH8= -sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/structured-merge-diff/v4 v4.2.0 h1:kDvPBbnPk+qYmkHmSo8vKGp438IASWofnbbUKDE/bv0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.0/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/connection/fake/mocks.go b/pkg/connection/fake/mocks.go new file mode 100644 index 000000000..ea8155b0e --- /dev/null +++ b/pkg/connection/fake/mocks.go @@ -0,0 +1,80 @@ +/* + 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 fake + +import ( + "context" + "encoding/json" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/connection/store" +) + +// SecretStore is a fake SecretStore +type SecretStore struct { + ReadKeyValuesFn func(ctx context.Context, n store.ScopedName, s *store.Secret) error + WriteKeyValuesFn func(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error) + DeleteKeyValuesFn func(ctx context.Context, s *store.Secret, do ...store.DeleteOption) error +} + +// ReadKeyValues reads key values. +func (ss *SecretStore) ReadKeyValues(ctx context.Context, n store.ScopedName, s *store.Secret) error { + return ss.ReadKeyValuesFn(ctx, n, s) +} + +// WriteKeyValues writes key values. +func (ss *SecretStore) WriteKeyValues(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error) { + return ss.WriteKeyValuesFn(ctx, s, wo...) +} + +// DeleteKeyValues deletes key values. +func (ss *SecretStore) DeleteKeyValues(ctx context.Context, s *store.Secret, do ...store.DeleteOption) error { + return ss.DeleteKeyValuesFn(ctx, s, do...) +} + +// StoreConfig is a mock implementation of the StoreConfig interface. +type StoreConfig struct { + metav1.ObjectMeta + + Config v1.SecretStoreConfig + v1.ConditionedStatus +} + +// GetStoreConfig returns SecretStoreConfig +func (s *StoreConfig) GetStoreConfig() v1.SecretStoreConfig { + return s.Config +} + +// GetObjectKind returns schema.ObjectKind. +func (s *StoreConfig) GetObjectKind() schema.ObjectKind { + return schema.EmptyObjectKind +} + +// DeepCopyObject returns a copy of the object as runtime.Object +func (s *StoreConfig) DeepCopyObject() runtime.Object { + out := &StoreConfig{} + j, err := json.Marshal(s) + if err != nil { + panic(err) + } + _ = json.Unmarshal(j, out) + return out +} diff --git a/pkg/connection/interfaces.go b/pkg/connection/interfaces.go new file mode 100644 index 000000000..64dd595dd --- /dev/null +++ b/pkg/connection/interfaces.go @@ -0,0 +1,39 @@ +/* +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 connection + +import ( + "context" + + v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/connection/store" + "github.com/crossplane/crossplane-runtime/pkg/resource" +) + +// A StoreConfig configures a connection store. +type StoreConfig interface { + resource.Object + + GetStoreConfig() v1.SecretStoreConfig +} + +// A Store stores sensitive key values in Secret. +type Store interface { + ReadKeyValues(ctx context.Context, n store.ScopedName, s *store.Secret) error + WriteKeyValues(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (changed bool, err error) + DeleteKeyValues(ctx context.Context, s *store.Secret, do ...store.DeleteOption) error +} diff --git a/pkg/connection/manager.go b/pkg/connection/manager.go new file mode 100644 index 000000000..d1944c453 --- /dev/null +++ b/pkg/connection/manager.go @@ -0,0 +1,215 @@ +/* +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 connection + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/connection/store" + "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/pkg/resource" +) + +// Error strings. +const ( + errConnectStore = "cannot connect to secret store" + errWriteStore = "cannot write to secret store" + errReadStore = "cannot read from secret store" + errDeleteFromStore = "cannot delete from secret store" + errGetStoreConfig = "cannot get store config" + errSecretConflict = "cannot establish control of existing connection secret" + + errFmtNotOwnedBy = "existing secret is not owned by UID %q" +) + +// StoreBuilderFn is a function that builds and returns a Store with a given +// store config. +type StoreBuilderFn func(ctx context.Context, local client.Client, cfg v1.SecretStoreConfig) (Store, error) + +// A DetailsManagerOption configures a DetailsManager. +type DetailsManagerOption func(*DetailsManager) + +// WithStoreBuilder configures the StoreBuilder to use. +func WithStoreBuilder(sb StoreBuilderFn) DetailsManagerOption { + return func(m *DetailsManager) { + m.storeBuilder = sb + } +} + +// DetailsManager is a connection details manager that satisfies the required +// interfaces to work with connection details by managing interaction with +// different store implementations. +type DetailsManager struct { + client client.Client + newConfig func() StoreConfig + storeBuilder StoreBuilderFn +} + +// NewDetailsManager returns a new connection DetailsManager. +func NewDetailsManager(c client.Client, of schema.GroupVersionKind, o ...DetailsManagerOption) *DetailsManager { + nc := func() StoreConfig { + return resource.MustCreateObject(of, c.Scheme()).(StoreConfig) + } + + // Panic early if we've been asked to reconcile a resource kind that has not + // been registered with our controller manager's scheme. + _ = nc() + + m := &DetailsManager{ + client: c, + newConfig: nc, + storeBuilder: RuntimeStoreBuilder, + } + + for _, mo := range o { + mo(m) + } + + return m +} + +// PublishConnection publishes the supplied ConnectionDetails to a secret on +// the configured connection Store. +func (m *DetailsManager) PublishConnection(ctx context.Context, so resource.ConnectionSecretOwner, conn managed.ConnectionDetails) (bool, error) { + // This resource does not want to expose a connection secret. + p := so.GetPublishConnectionDetailsTo() + if p == nil { + return false, nil + } + + ss, err := m.connectStore(ctx, p) + if err != nil { + return false, errors.Wrap(err, errConnectStore) + } + + changed, err := ss.WriteKeyValues(ctx, store.NewSecret(so, store.KeyValues(conn)), SecretToWriteMustBeOwnedBy(so)) + return changed, errors.Wrap(err, errWriteStore) +} + +// UnpublishConnection deletes connection details secret to the configured +// connection Store. +func (m *DetailsManager) UnpublishConnection(ctx context.Context, so resource.ConnectionSecretOwner, conn managed.ConnectionDetails) error { + // This resource didn't expose a connection secret. + p := so.GetPublishConnectionDetailsTo() + if p == nil { + return nil + } + + ss, err := m.connectStore(ctx, p) + if err != nil { + return errors.Wrap(err, errConnectStore) + } + + return errors.Wrap(ss.DeleteKeyValues(ctx, store.NewSecret(so, store.KeyValues(conn)), SecretToDeleteMustBeOwnedBy(so)), errDeleteFromStore) +} + +// FetchConnection fetches connection details of a given ConnectionSecretOwner. +func (m *DetailsManager) FetchConnection(ctx context.Context, so resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) { + // This resource does not want to expose a connection secret. + p := so.GetPublishConnectionDetailsTo() + if p == nil { + return nil, nil + } + + ss, err := m.connectStore(ctx, p) + if err != nil { + return nil, errors.Wrap(err, errConnectStore) + } + + s := &store.Secret{} + return managed.ConnectionDetails(s.Data), errors.Wrap(ss.ReadKeyValues(ctx, store.ScopedName{Name: p.Name, Scope: so.GetNamespace()}, s), errReadStore) +} + +// PropagateConnection propagate connection details from one resource to another. +func (m *DetailsManager) PropagateConnection(ctx context.Context, to resource.LocalConnectionSecretOwner, from resource.ConnectionSecretOwner) (propagated bool, err error) { // nolint:interfacer + // NOTE(turkenh): Had to add linter exception for "interfacer" suggestion + // to use "store.SecretOwner" as the type of "to" parameter. We want to + // keep it as "resource.LocalConnectionSecretOwner" to satisfy the + // ConnectionPropagater interface for XR Claims. + + // Either from does not expose a connection secret, or to does not want one. + if from.GetPublishConnectionDetailsTo() == nil || to.GetPublishConnectionDetailsTo() == nil { + return false, nil + } + + ssFrom, err := m.connectStore(ctx, from.GetPublishConnectionDetailsTo()) + if err != nil { + return false, errors.Wrap(err, errConnectStore) + } + + sFrom := &store.Secret{} + if err = ssFrom.ReadKeyValues(ctx, store.ScopedName{ + Name: from.GetPublishConnectionDetailsTo().Name, + Scope: from.GetNamespace(), + }, sFrom); err != nil { + return false, errors.Wrap(err, errReadStore) + } + + // Make sure 'from' is the controller of the connection secret it references + // before we propagate it. This ensures a resource cannot use Crossplane to + // circumvent RBAC by propagating a secret it does not own. + if sFrom.GetOwner() != string(from.GetUID()) { + return false, errors.New(errSecretConflict) + } + + ssTo, err := m.connectStore(ctx, to.GetPublishConnectionDetailsTo()) + if err != nil { + return false, errors.Wrap(err, errConnectStore) + } + + changed, err := ssTo.WriteKeyValues(ctx, store.NewSecret(to, sFrom.Data), SecretToWriteMustBeOwnedBy(to)) + return changed, errors.Wrap(err, errWriteStore) +} + +func (m *DetailsManager) connectStore(ctx context.Context, p *v1.PublishConnectionDetailsTo) (Store, error) { + sc := m.newConfig() + if err := m.client.Get(ctx, types.NamespacedName{Name: p.SecretStoreConfigRef.Name}, sc); err != nil { + return nil, errors.Wrap(err, errGetStoreConfig) + } + + return m.storeBuilder(ctx, m.client, sc.GetStoreConfig()) +} + +// SecretToWriteMustBeOwnedBy requires that the current object is a +// connection secret that is owned by an object with the supplied UID. +func SecretToWriteMustBeOwnedBy(so metav1.Object) store.WriteOption { + return func(_ context.Context, current, _ *store.Secret) error { + return secretMustBeOwnedBy(so, current) + } +} + +// SecretToDeleteMustBeOwnedBy requires that the current secret is owned by +// an object with the supplied UID. +func SecretToDeleteMustBeOwnedBy(so metav1.Object) store.DeleteOption { + return func(_ context.Context, secret *store.Secret) error { + return secretMustBeOwnedBy(so, secret) + } +} + +func secretMustBeOwnedBy(so metav1.Object, secret *store.Secret) error { + if secret.Metadata == nil || secret.Metadata.GetOwnerUID() != string(so.GetUID()) { + return errors.Errorf(errFmtNotOwnedBy, string(so.GetUID())) + } + return nil +} diff --git a/pkg/connection/manager_test.go b/pkg/connection/manager_test.go new file mode 100644 index 000000000..b311742db --- /dev/null +++ b/pkg/connection/manager_test.go @@ -0,0 +1,1235 @@ +/* + 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 connection + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + + v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/connection/fake" + "github.com/crossplane/crossplane-runtime/pkg/connection/store" + "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/pkg/resource" + resourcefake "github.com/crossplane/crossplane-runtime/pkg/resource/fake" + "github.com/crossplane/crossplane-runtime/pkg/test" +) + +const ( + secretStoreFake = v1.SecretStoreType("Fake") + fakeConfig = "fake" + testUID = "e8587e99-15c9-4069-a530-1d2205032848" +) + +const ( + errBuildStore = "cannot build store" +) + +var ( + fakeStore = secretStoreFake + + errBoom = errors.New("boom") +) + +func TestManagerConnectStore(t *testing.T) { + type args struct { + c client.Client + sb StoreBuilderFn + + p *v1.PublishConnectionDetailsTo + } + + type want struct { + err error + } + + cases := map[string]struct { + reason string + args + want + }{ + "ConfigNotFound": { + reason: "We should return a proper error if referenced StoreConfig does not exist.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + return kerrors.NewNotFound(schema.GroupResource{}, key.Name) + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{}), + p: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + want: want{ + err: errors.Wrapf(kerrors.NewNotFound(schema.GroupResource{}, fakeConfig), errGetStoreConfig), + }, + }, + "BuildStoreError": { + reason: "We should return any error encountered while building the Store.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + *obj.(*fake.StoreConfig) = fake.StoreConfig{} + return nil + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: func(ctx context.Context, local client.Client, cfg v1.SecretStoreConfig) (Store, error) { + return nil, errors.New(errBuildStore) + }, + p: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + want: want{ + err: errors.New(errBuildStore), + }, + }, + "SuccessfulConnect": { + reason: "We should not return an error when connected successfully.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{}), + p: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + want: want{ + err: nil, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + m := NewDetailsManager(tc.args.c, resourcefake.GVK(&fake.StoreConfig{}), WithStoreBuilder(tc.args.sb)) + + _, err := m.connectStore(context.Background(), tc.args.p) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\nReason: %s\nm.connectStore(...): -want error, +got error:\n%s", tc.reason, diff) + } + }) + } +} + +func TestManagerPublishConnection(t *testing.T) { + type args struct { + c client.Client + sb StoreBuilderFn + + conn managed.ConnectionDetails + so resource.ConnectionSecretOwner + } + + type want struct { + published bool + err error + } + + cases := map[string]struct { + reason string + args + want + }{ + "NoConnectionDetails": { + reason: "We should return no error if resource does not want to expose a connection secret.", + args: args{ + c: &test.MockClient{ + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + so: &resourcefake.MockConnectionSecretOwner{To: nil}, + }, + want: want{ + err: nil, + }, + }, + "CannotConnect": { + reason: "We should return any error encountered while connecting to Store.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + return kerrors.NewNotFound(schema.GroupResource{}, key.Name) + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + WriteKeyValuesFn: func(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error) { + return false, nil + }, + }), + so: &resourcefake.MockConnectionSecretOwner{ + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: "non-existing", + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errors.Wrapf(kerrors.NewNotFound(schema.GroupResource{}, "non-existing"), errGetStoreConfig), errConnectStore), + }, + }, + "CannotPublishTo": { + reason: "We should return a proper error when publish to secret store failed.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + WriteKeyValuesFn: func(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error) { + return false, errBoom + }, + }), + so: &resourcefake.MockConnectionSecretOwner{ + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errWriteStore), + }, + }, + "SuccessfulPublishWithOwnerUID": { + reason: "We should return no error when published successfully.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + WriteKeyValuesFn: func(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error) { + if diff := cmp.Diff(testUID, s.Metadata.GetOwnerUID()); diff != "" { + t.Errorf("\nReason: %s\nm.publishConnection(...): -want ownerUID, +got ownerUID:\n%s", testUID, diff) + } + return true, nil + }, + }), + so: &resourcefake.MockConnectionSecretOwner{ + ObjectMeta: metav1.ObjectMeta{ + UID: testUID, + }, + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + WriterTo: nil, + }, + }, + want: want{ + published: true, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + m := NewDetailsManager(tc.args.c, resourcefake.GVK(&fake.StoreConfig{}), WithStoreBuilder(tc.args.sb)) + + published, err := m.PublishConnection(context.Background(), tc.args.so, tc.args.conn) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\nReason: %s\nm.publishConnection(...): -want error, +got error:\n%s", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.published, published); diff != "" { + t.Errorf("\nReason: %s\nm.publishConnection(...): -want published, +got published:\n%s", tc.reason, diff) + } + }) + } +} + +func TestManagerUnpublishConnection(t *testing.T) { + type args struct { + c client.Client + sb StoreBuilderFn + + conn managed.ConnectionDetails + so resource.ConnectionSecretOwner + } + + type want struct { + err error + } + + cases := map[string]struct { + reason string + args + want + }{ + "NoConnectionDetails": { + reason: "We should return no error if resource does not want to expose a connection secret.", + args: args{ + c: &test.MockClient{ + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + so: &resourcefake.MockConnectionSecretOwner{To: nil}, + }, + want: want{ + err: nil, + }, + }, + "CannotConnect": { + reason: "We should return any error encountered while connecting to Store.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + return kerrors.NewNotFound(schema.GroupResource{}, key.Name) + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + WriteKeyValuesFn: func(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error) { + return false, nil + }, + }), + so: &resourcefake.MockConnectionSecretOwner{ + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: "non-existing", + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errors.Wrapf(kerrors.NewNotFound(schema.GroupResource{}, "non-existing"), errGetStoreConfig), errConnectStore), + }, + }, + "CannotUnpublish": { + reason: "We should return a proper error when delete from secret store failed.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + DeleteKeyValuesFn: func(ctx context.Context, s *store.Secret, do ...store.DeleteOption) error { + return errBoom + }, + }), + so: &resourcefake.MockConnectionSecretOwner{ + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errDeleteFromStore), + }, + }, + "CannotUnpublishUnowned": { + reason: "We should return a proper error when attempted to unpublish a secret that is not owned.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + DeleteKeyValuesFn: func(ctx context.Context, s *store.Secret, do ...store.DeleteOption) error { + s.Metadata = &v1.ConnectionSecretMetadata{ + Labels: map[string]string{ + v1.LabelKeyOwnerUID: "00000000-1111-2222-3333-444444444444", + }, + } + for _, o := range do { + if err := o(ctx, s); err != nil { + return err + } + } + return nil + }, + }), + so: &resourcefake.MockConnectionSecretOwner{ + ObjectMeta: metav1.ObjectMeta{ + UID: testUID, + }, + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errors.Errorf(errFmtNotOwnedBy, testUID), errDeleteFromStore), + }, + }, + "SuccessfulUnpublish": { + reason: "We should return no error when unpublished successfully.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + DeleteKeyValuesFn: func(ctx context.Context, s *store.Secret, do ...store.DeleteOption) error { + s.Metadata = &v1.ConnectionSecretMetadata{ + Labels: map[string]string{ + v1.LabelKeyOwnerUID: testUID, + }, + } + for _, o := range do { + if err := o(ctx, s); err != nil { + return err + } + } + return nil + }, + }), + so: &resourcefake.MockConnectionSecretOwner{ + ObjectMeta: metav1.ObjectMeta{ + UID: testUID, + }, + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + }, + want: want{ + err: nil, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + m := NewDetailsManager(tc.args.c, resourcefake.GVK(&fake.StoreConfig{}), WithStoreBuilder(tc.args.sb)) + + err := m.UnpublishConnection(context.Background(), tc.args.so, tc.args.conn) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\nReason: %s\nm.unpublishConnection(...): -want error, +got error:\n%s", tc.reason, diff) + } + }) + } +} + +func TestManagerFetchConnection(t *testing.T) { + type args struct { + c client.Client + sb StoreBuilderFn + + so resource.ConnectionSecretOwner + } + + type want struct { + conn managed.ConnectionDetails + err error + } + + cases := map[string]struct { + reason string + args + want + }{ + "NoConnectionDetails": { + reason: "We should return no error if resource does not want to expose a connection secret.", + args: args{ + c: &test.MockClient{ + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + so: &resourcefake.MockConnectionSecretOwner{To: nil}, + }, + want: want{ + err: nil, + }, + }, + "CannotConnect": { + reason: "We should return any error encountered while connecting to Store.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + return kerrors.NewNotFound(schema.GroupResource{}, key.Name) + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + WriteKeyValuesFn: func(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error) { + return false, nil + }, + }), + so: &resourcefake.MockConnectionSecretOwner{ + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: "non-existing", + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errors.Wrapf(kerrors.NewNotFound(schema.GroupResource{}, "non-existing"), errGetStoreConfig), errConnectStore), + }, + }, + "CannotFetch": { + reason: "We should return a proper error when fetch from secret store failed.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + ReadKeyValuesFn: func(ctx context.Context, n store.ScopedName, s *store.Secret) error { + return errBoom + }, + }), + so: &resourcefake.MockConnectionSecretOwner{ + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errReadStore), + }, + }, + "SuccessfulFetch": { + reason: "We should return no error when fetched successfully.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + ReadKeyValuesFn: func(ctx context.Context, n store.ScopedName, s *store.Secret) error { + s.Data = store.KeyValues{ + "key1": []byte("val1"), + } + return nil + }, + }), + so: &resourcefake.MockConnectionSecretOwner{ + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + }, + want: want{ + conn: map[string][]byte{ + "key1": []byte("val1"), + }, + err: nil, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + m := NewDetailsManager(tc.args.c, resourcefake.GVK(&fake.StoreConfig{}), WithStoreBuilder(tc.args.sb)) + + got, err := m.FetchConnection(context.Background(), tc.args.so) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\nReason: %s\nm.FetchConnection(...): -want error, +got error:\n%s", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.conn, got); diff != "" { + t.Errorf("\nReason: %s\nm.FetchConnection(...): -want error, +got error:\n%s", tc.reason, diff) + } + }) + } +} + +func TestManagerPropagateConnection(t *testing.T) { + type args struct { + c client.Client + sb StoreBuilderFn + + to resource.LocalConnectionSecretOwner + from resource.ConnectionSecretOwner + } + + type want struct { + propagated bool + err error + } + + cases := map[string]struct { + reason string + args + want + }{ + "NoConnectionDetailsSource": { + reason: "We should return no error if source resource does not want to expose a connection secret.", + args: args{ + c: &test.MockClient{ + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + from: &resourcefake.MockConnectionSecretOwner{To: nil}, + }, + want: want{ + err: nil, + }, + }, + "NoConnectionDetailsDestination": { + reason: "We should return no error if destination resource does not want to expose a connection secret.", + args: args{ + c: &test.MockClient{ + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + from: &resourcefake.MockConnectionSecretOwner{To: &v1.PublishConnectionDetailsTo{}}, + to: &resourcefake.MockLocalConnectionSecretOwner{To: nil}, + }, + want: want{ + err: nil, + }, + }, + "CannotConnectSource": { + reason: "We should return any error encountered while connecting to Source Store.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + return kerrors.NewNotFound(schema.GroupResource{}, key.Name) + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + WriteKeyValuesFn: func(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error) { + return false, nil + }, + }), + from: &resourcefake.MockConnectionSecretOwner{ + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: "non-existing", + }, + }, + }, + to: &resourcefake.MockLocalConnectionSecretOwner{To: &v1.PublishConnectionDetailsTo{}}, + }, + want: want{ + err: errors.Wrap(errors.Wrapf(kerrors.NewNotFound(schema.GroupResource{}, "non-existing"), errGetStoreConfig), errConnectStore), + }, + }, + "CannotFetch": { + reason: "We should return a proper error when fetch from secret store failed.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + ReadKeyValuesFn: func(ctx context.Context, n store.ScopedName, s *store.Secret) error { + return errBoom + }, + }), + from: &resourcefake.MockConnectionSecretOwner{ + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + to: &resourcefake.MockLocalConnectionSecretOwner{To: &v1.PublishConnectionDetailsTo{}}, + }, + want: want{ + err: errors.Wrap(errBoom, errReadStore), + }, + }, + "CannotEstablishControlOfUnowned": { + reason: "We should return a proper error if source secret is not owned by any resource", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + if key.Name == fakeConfig { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + } + + return kerrors.NewNotFound(schema.GroupResource{}, "non-existing") + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + ReadKeyValuesFn: func(ctx context.Context, n store.ScopedName, s *store.Secret) error { + s.Metadata = &v1.ConnectionSecretMetadata{} + return nil + }, + }), + from: &resourcefake.MockConnectionSecretOwner{ + ObjectMeta: metav1.ObjectMeta{ + UID: testUID, + }, + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + WriterTo: nil, + }, + to: &resourcefake.MockLocalConnectionSecretOwner{ + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + }, + want: want{ + err: errors.New(errSecretConflict), + }, + }, + "CannotEstablishControlOfAnotherOwner": { + reason: "We should return a proper error if source secret is owned by another resource", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + if key.Name == fakeConfig { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + } + + return kerrors.NewNotFound(schema.GroupResource{}, "non-existing") + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + ReadKeyValuesFn: func(ctx context.Context, n store.ScopedName, s *store.Secret) error { + s.Metadata = &v1.ConnectionSecretMetadata{ + Labels: map[string]string{ + v1.LabelKeyOwnerUID: "00000000-1111-2222-3333-444444444444", + }, + } + return nil + }, + }), + from: &resourcefake.MockConnectionSecretOwner{ + ObjectMeta: metav1.ObjectMeta{ + UID: testUID, + }, + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + WriterTo: nil, + }, + to: &resourcefake.MockLocalConnectionSecretOwner{ + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + }, + want: want{ + err: errors.New(errSecretConflict), + }, + }, + "CannotConnectDestination": { + reason: "We should return any error encountered while connecting to Destination Store.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + if key.Name == fakeConfig { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + } + + return kerrors.NewNotFound(schema.GroupResource{}, "non-existing") + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + ReadKeyValuesFn: func(ctx context.Context, n store.ScopedName, s *store.Secret) error { + s.Metadata = &v1.ConnectionSecretMetadata{ + Labels: map[string]string{ + v1.LabelKeyOwnerUID: testUID, + }, + } + return nil + }, + }), + from: &resourcefake.MockConnectionSecretOwner{ + ObjectMeta: metav1.ObjectMeta{ + UID: testUID, + }, + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + WriterTo: nil, + }, + to: &resourcefake.MockLocalConnectionSecretOwner{ + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: "non-existing", + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errors.Wrapf(kerrors.NewNotFound(schema.GroupResource{}, "non-existing"), errGetStoreConfig), errConnectStore), + }, + }, + "CannotPublish": { + reason: "We should return any error encountered while publishing to Destination Store.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + ReadKeyValuesFn: func(ctx context.Context, n store.ScopedName, s *store.Secret) error { + s.Metadata = &v1.ConnectionSecretMetadata{ + Labels: map[string]string{ + v1.LabelKeyOwnerUID: testUID, + }, + } + return nil + }, + WriteKeyValuesFn: func(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error) { + return false, errBoom + }, + }), + from: &resourcefake.MockConnectionSecretOwner{ + ObjectMeta: metav1.ObjectMeta{ + UID: testUID, + }, + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + to: &resourcefake.MockLocalConnectionSecretOwner{ + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errWriteStore), + }, + }, + "DestinationSecretCannotBeOwned": { + reason: "We should return a proper error if destination secret cannot be owned by destination resource.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + ReadKeyValuesFn: func(ctx context.Context, n store.ScopedName, s *store.Secret) error { + s.Metadata = &v1.ConnectionSecretMetadata{ + Labels: map[string]string{ + v1.LabelKeyOwnerUID: testUID, + }, + } + return nil + }, + WriteKeyValuesFn: func(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error) { + for _, o := range wo { + if err := o(context.Background(), &store.Secret{ + Metadata: &v1.ConnectionSecretMetadata{ + Labels: map[string]string{ + v1.LabelKeyOwnerUID: "00000000-1111-2222-3333-444444444444", + }, + }, + }, s); err != nil { + return false, err + } + } + return true, nil + }, + }), + from: &resourcefake.MockConnectionSecretOwner{ + ObjectMeta: metav1.ObjectMeta{ + UID: testUID, + }, + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + to: &resourcefake.MockLocalConnectionSecretOwner{ + ObjectMeta: metav1.ObjectMeta{ + UID: testUID, + }, + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errors.Errorf(errFmtNotOwnedBy, testUID), errWriteStore), + }, + }, + "SuccessfulPropagateCreated": { + reason: "We should return no error when propagated successfully.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + ReadKeyValuesFn: func(ctx context.Context, n store.ScopedName, s *store.Secret) error { + s.Metadata = &v1.ConnectionSecretMetadata{ + Labels: map[string]string{ + v1.LabelKeyOwnerUID: testUID, + }, + } + return nil + }, + WriteKeyValuesFn: func(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error) { + return true, nil + }, + }), + from: &resourcefake.MockConnectionSecretOwner{ + ObjectMeta: metav1.ObjectMeta{ + UID: testUID, + }, + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + to: &resourcefake.MockLocalConnectionSecretOwner{ + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + }, + want: want{ + propagated: true, + }, + }, + "CannotPropagateToUnowned": { + reason: "We should return a proper error when attempted to update an unowned secret.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + ReadKeyValuesFn: func(ctx context.Context, n store.ScopedName, s *store.Secret) error { + s.Metadata = &v1.ConnectionSecretMetadata{ + Labels: map[string]string{ + v1.LabelKeyOwnerUID: testUID, + }, + } + return nil + }, + WriteKeyValuesFn: func(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error) { + for _, o := range wo { + if err := o(context.Background(), &store.Secret{ + Data: map[string][]byte{ + "some-key": []byte("some-val"), + }, + }, s); err != nil { + return false, err + } + } + return true, nil + }, + }), + from: &resourcefake.MockConnectionSecretOwner{ + ObjectMeta: metav1.ObjectMeta{ + UID: testUID, + }, + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + to: &resourcefake.MockLocalConnectionSecretOwner{ + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + }, + want: want{ + propagated: false, + err: errors.Wrap(errors.Errorf(errFmtNotOwnedBy, ""), errWriteStore), + }, + }, + "SuccessfulPropagateUpdated": { + reason: "We should return no error when propagated successfully by updating an already owned secret.", + args: args{ + c: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + *obj.(*fake.StoreConfig) = fake.StoreConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeConfig, + }, + Config: v1.SecretStoreConfig{ + Type: &fakeStore, + }, + } + return nil + }, + MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})), + }, + sb: fakeStoreBuilderFn(fake.SecretStore{ + ReadKeyValuesFn: func(ctx context.Context, n store.ScopedName, s *store.Secret) error { + s.Metadata = &v1.ConnectionSecretMetadata{ + Labels: map[string]string{ + v1.LabelKeyOwnerUID: testUID, + }, + } + return nil + }, + WriteKeyValuesFn: func(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error) { + for _, o := range wo { + if err := o(context.Background(), &store.Secret{ + Metadata: &v1.ConnectionSecretMetadata{ + Labels: map[string]string{ + v1.LabelKeyOwnerUID: testUID, + }, + }, + Data: map[string][]byte{ + "some-key": []byte("some-val"), + }, + }, s); err != nil { + return false, err + } + } + return true, nil + }, + }), + from: &resourcefake.MockConnectionSecretOwner{ + ObjectMeta: metav1.ObjectMeta{ + UID: testUID, + }, + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + to: &resourcefake.MockLocalConnectionSecretOwner{ + ObjectMeta: metav1.ObjectMeta{ + UID: testUID, + }, + To: &v1.PublishConnectionDetailsTo{ + SecretStoreConfigRef: &v1.Reference{ + Name: fakeConfig, + }, + }, + }, + }, + want: want{ + propagated: true, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + m := NewDetailsManager(tc.args.c, resourcefake.GVK(&fake.StoreConfig{}), WithStoreBuilder(tc.args.sb)) + + got, err := m.PropagateConnection(context.Background(), tc.args.to, tc.args.from) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\nReason: %s\nm.PropagateConnection(...): -want error, +got error:\n%s", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.propagated, got); diff != "" { + t.Errorf("\nReason: %s\nm.PropagateConnection(...): -want propagated, +got propagated:\n%s", tc.reason, diff) + } + }) + } +} + +func fakeStoreBuilderFn(ss fake.SecretStore) StoreBuilderFn { + return func(_ context.Context, _ client.Client, cfg v1.SecretStoreConfig) (Store, error) { + if *cfg.Type == fakeStore { + return &ss, nil + } + return nil, errors.Errorf(errFmtUnknownSecretStore, *cfg.Type) + } +} diff --git a/pkg/connection/store/kubernetes/store.go b/pkg/connection/store/kubernetes/store.go new file mode 100644 index 000000000..302fae2f6 --- /dev/null +++ b/pkg/connection/store/kubernetes/store.go @@ -0,0 +1,241 @@ +/* +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 kubernetes + +import ( + "context" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" + + v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/connection/store" + "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/resource" +) + +// Error strings. +const ( + errGetSecret = "cannot get secret" + errDeleteSecret = "cannot delete secret" + errUpdateSecret = "cannot update secret" + errApplySecret = "cannot apply secret" + + errExtractKubernetesAuthCreds = "cannot extract kubernetes auth credentials" + errBuildRestConfig = "cannot build rest config kubeconfig" + errBuildClient = "cannot build Kubernetes client" +) + +// SecretStore is a Kubernetes Secret Store. +type SecretStore struct { + client resource.ClientApplicator + + defaultNamespace string +} + +// NewSecretStore returns a new Kubernetes SecretStore. +func NewSecretStore(ctx context.Context, local client.Client, cfg v1.SecretStoreConfig) (*SecretStore, error) { + kube, err := buildClient(ctx, local, cfg) + if err != nil { + return nil, errors.Wrap(err, errBuildClient) + } + + return &SecretStore{ + client: resource.ClientApplicator{ + Client: kube, + Applicator: resource.NewApplicatorWithRetry(resource.NewAPIPatchingApplicator(kube), resource.IsAPIErrorWrapped, nil), + }, + defaultNamespace: cfg.DefaultScope, + }, nil +} + +func buildClient(ctx context.Context, local client.Client, cfg v1.SecretStoreConfig) (client.Client, error) { + if cfg.Kubernetes == nil { + // No KubernetesSecretStoreConfig provided, local API Server will be + // used as Secret Store. + return local, nil + } + // Configure client for an external API server with a given Kubeconfig. + kfg, err := resource.CommonCredentialExtractor(ctx, cfg.Kubernetes.Auth.Source, local, cfg.Kubernetes.Auth.CommonCredentialSelectors) + if err != nil { + return nil, errors.Wrap(err, errExtractKubernetesAuthCreds) + } + config, err := clientcmd.RESTConfigFromKubeConfig(kfg) + if err != nil { + return nil, errors.Wrap(err, errBuildRestConfig) + } + return client.New(config, client.Options{}) +} + +// ReadKeyValues reads and returns key value pairs for a given Kubernetes Secret. +func (ss *SecretStore) ReadKeyValues(ctx context.Context, n store.ScopedName, s *store.Secret) error { + ks := &corev1.Secret{} + if err := ss.client.Get(ctx, types.NamespacedName{Name: n.Name, Namespace: ss.namespaceForSecret(n)}, ks); err != nil { + return errors.Wrap(err, errGetSecret) + } + s.Data = ks.Data + s.Metadata = &v1.ConnectionSecretMetadata{ + Labels: ks.Labels, + Annotations: ks.Annotations, + Type: &ks.Type, + } + return nil +} + +// WriteKeyValues writes key value pairs to a given Kubernetes Secret. +func (ss *SecretStore) WriteKeyValues(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error) { + ks := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: s.Name, + Namespace: ss.namespaceForSecret(s.ScopedName), + }, + Type: resource.SecretTypeConnection, + Data: s.Data, + } + + if s.Metadata != nil { + ks.Labels = s.Metadata.Labels + ks.Annotations = s.Metadata.Annotations + if s.Metadata.Type != nil { + ks.Type = *s.Metadata.Type + } + } + + ao := applyOptions(wo...) + ao = append(ao, resource.AllowUpdateIf(func(current, desired runtime.Object) bool { + // We consider the update to be a no-op and don't allow it if the + // current and existing secret data are identical. + return !cmp.Equal(current.(*corev1.Secret).Data, desired.(*corev1.Secret).Data, cmpopts.EquateEmpty()) + })) + + err := ss.client.Apply(ctx, ks, ao...) + if resource.IsNotAllowed(err) { + // The update was not allowed because it was a no-op. + return false, nil + } + if err != nil { + return false, errors.Wrap(err, errApplySecret) + } + return true, nil +} + +// DeleteKeyValues delete key value pairs from a given Kubernetes Secret. +// If no kv specified, the whole secret instance is deleted. +// If kv specified, those would be deleted and secret instance will be deleted +// only if there is no data left. +func (ss *SecretStore) DeleteKeyValues(ctx context.Context, s *store.Secret, do ...store.DeleteOption) error { + // NOTE(turkenh): DeleteKeyValues method wouldn't need to do anything if we + // have used owner references similar to existing implementation. However, + // this wouldn't work if the K8s API is not the same as where ConnectionSecretOwner + // object lives, i.e. a remote cluster. + // Considering there is not much additional value with deletion via garbage + // collection in this specific case other than one less API call during + // deletion, I opted for unifying both instead of adding conditional logic + // like add owner references if not remote and not call delete etc. + ks := &corev1.Secret{} + err := ss.client.Get(ctx, types.NamespacedName{Name: s.Name, Namespace: ss.namespaceForSecret(s.ScopedName)}, ks) + if kerrors.IsNotFound(err) { + // Secret already deleted, nothing to do. + return nil + } + if err != nil { + return errors.Wrap(err, errGetSecret) + } + + for _, o := range do { + if err = o(ctx, s); err != nil { + return err + } + } + + // Delete all supplied keys from secret data + for k := range s.Data { + delete(ks.Data, k) + } + if len(s.Data) == 0 || len(ks.Data) == 0 { + // Secret is deleted only if: + // - No kv to delete specified as input + // - No data left in the secret + return errors.Wrapf(ss.client.Delete(ctx, ks), errDeleteSecret) + } + // If there are still keys left, update the secret with the remaining. + return errors.Wrapf(ss.client.Update(ctx, ks), errUpdateSecret) +} + +func (ss *SecretStore) namespaceForSecret(n store.ScopedName) string { + if n.Scope == "" { + return ss.defaultNamespace + } + return n.Scope +} + +func applyOptions(wo ...store.WriteOption) []resource.ApplyOption { + ao := make([]resource.ApplyOption, len(wo)) + for i := range wo { + o := wo[i] + ao[i] = func(ctx context.Context, current, desired runtime.Object) error { + currentSecret := current.(*corev1.Secret) + desiredSecret := desired.(*corev1.Secret) + + cs := &store.Secret{ + ScopedName: store.ScopedName{ + Name: currentSecret.Name, + Scope: currentSecret.Namespace, + }, + Metadata: &v1.ConnectionSecretMetadata{ + Labels: currentSecret.Labels, + Annotations: currentSecret.Annotations, + Type: ¤tSecret.Type, + }, + Data: currentSecret.Data, + } + ds := &store.Secret{ + ScopedName: store.ScopedName{ + Name: desiredSecret.Name, + Scope: desiredSecret.Namespace, + }, + Metadata: &v1.ConnectionSecretMetadata{ + Labels: desiredSecret.Labels, + Annotations: desiredSecret.Annotations, + Type: &desiredSecret.Type, + }, + Data: desiredSecret.Data, + } + + if err := o(ctx, cs, ds); err != nil { + return err + } + + desiredSecret.Data = ds.Data + desiredSecret.Labels = ds.Metadata.Labels + desiredSecret.Annotations = ds.Metadata.Annotations + if ds.Metadata.Type != nil { + desiredSecret.Type = *ds.Metadata.Type + } + + return nil + } + } + return ao +} diff --git a/pkg/connection/store/kubernetes/store_test.go b/pkg/connection/store/kubernetes/store_test.go new file mode 100644 index 000000000..e3363903b --- /dev/null +++ b/pkg/connection/store/kubernetes/store_test.go @@ -0,0 +1,826 @@ +/* + 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 kubernetes + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + + v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/connection/store" + "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/resource" + "github.com/crossplane/crossplane-runtime/pkg/test" +) + +var ( + errBoom = errors.New("boom") + + fakeSecretName = "fake" + fakeSecretNamespace = "fake-namespace" + + storeTypeKubernetes = v1.SecretStoreKubernetes +) + +func fakeKV() map[string][]byte { + return map[string][]byte{ + "key1": []byte("value1"), + "key2": []byte("value2"), + "key3": []byte("value3"), + } +} + +func fakeLabels() map[string]string { + return map[string]string{ + "environment": "unit-test", + "reason": "testing", + } +} + +func fakeAnnotations() map[string]string { + return map[string]string{ + "some-annotation-key": "some-annotation-value", + } +} + +func TestSecretStoreReadKeyValues(t *testing.T) { + type args struct { + client resource.ClientApplicator + n store.ScopedName + } + type want struct { + result store.KeyValues + err error + } + + cases := map[string]struct { + reason string + args + want + }{ + "CannotGetSecret": { + reason: "Should return a proper error if cannot get the secret", + args: args{ + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(errBoom), + }, + }, + n: store.ScopedName{ + Name: fakeSecretName, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errGetSecret), + }, + }, + "SuccessfulRead": { + reason: "Should return all key values after a success read", + args: args{ + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + *obj.(*corev1.Secret) = corev1.Secret{ + Data: fakeKV(), + } + return nil + }), + }, + }, + n: store.ScopedName{ + Name: fakeSecretName, + }, + }, + want: want{ + result: store.KeyValues(fakeKV()), + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + ss := &SecretStore{ + client: tc.args.client, + } + + s := &store.Secret{} + err := ss.ReadKeyValues(context.Background(), tc.args.n, s) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nss.ReadKeyValues(...): -want error, +got error:\n%s", tc.reason, diff) + } + + if diff := cmp.Diff(tc.want.result, s.Data); diff != "" { + t.Errorf("\n%s\nss.ReadKeyValues(...): -want, +got:\n%s", tc.reason, diff) + } + }) + } +} + +func TestSecretStoreWriteKeyValues(t *testing.T) { + secretTypeOpaque := corev1.SecretTypeOpaque + type args struct { + client resource.ClientApplicator + defaultNamespace string + secret *store.Secret + + wo []store.WriteOption + } + type want struct { + changed bool + err error + } + + cases := map[string]struct { + reason string + args + want + }{ + "ApplyFailed": { + reason: "Should return a proper error when cannot apply.", + args: args{ + client: resource.ClientApplicator{ + Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error { + return errBoom + }), + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: fakeSecretName, + Scope: fakeSecretNamespace, + }, + Data: store.KeyValues(fakeKV()), + }, + }, + want: want{ + err: errors.Wrap(errBoom, errApplySecret), + }, + }, + "FailedWriteOption": { + reason: "Should return a proper error if supplied write option fails", + args: args{ + client: resource.ClientApplicator{ + Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error { + for _, fn := range option { + if err := fn(ctx, fakeConnectionSecret(withData(fakeKV())), obj); err != nil { + return err + } + } + return nil + }), + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: fakeSecretName, + Scope: fakeSecretNamespace, + }, + Data: store.KeyValues(fakeKV()), + }, + wo: []store.WriteOption{ + func(ctx context.Context, current, desired *store.Secret) error { + return errBoom + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errApplySecret), + }, + }, + "SuccessfulWriteOption": { + reason: "Should return a proper error if supplied write option fails", + args: args{ + client: resource.ClientApplicator{ + Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error { + for _, fn := range option { + if err := fn(ctx, fakeConnectionSecret(withData(fakeKV())), obj); err != nil { + return err + } + } + return nil + }), + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: fakeSecretName, + Scope: fakeSecretNamespace, + }, + Data: store.KeyValues(fakeKV()), + }, + wo: []store.WriteOption{ + func(ctx context.Context, current, desired *store.Secret) error { + desired.Data["customkey"] = []byte("customval") + desired.Metadata = &v1.ConnectionSecretMetadata{ + Labels: map[string]string{ + "foo": "baz", + }, + } + return nil + }, + }, + }, + want: want{ + changed: true, + }, + }, + "SecretAlreadyUpToDate": { + reason: "Should not change secret if already up to date.", + args: args{ + client: resource.ClientApplicator{ + Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error { + for _, fn := range option { + if err := fn(ctx, fakeConnectionSecret(withData(fakeKV())), obj); err != nil { + return err + } + } + return nil + }), + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: fakeSecretName, + Scope: fakeSecretNamespace, + }, + Data: store.KeyValues(fakeKV()), + }, + }, + }, + "SecretUpdatedWithNewValue": { + reason: "Should update value for an existing key if changed.", + args: args{ + client: resource.ClientApplicator{ + Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error { + if diff := cmp.Diff(fakeConnectionSecret(withData(map[string][]byte{ + "existing-key": []byte("new-value"), + })), obj.(*corev1.Secret)); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + for _, fn := range option { + if err := fn(ctx, fakeConnectionSecret(withData(map[string][]byte{ + "existing-key": []byte("old-value"), + })), obj); err != nil { + return err + } + } + return nil + }), + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: fakeSecretName, + Scope: fakeSecretNamespace, + }, + Data: store.KeyValues(map[string][]byte{ + "existing-key": []byte("new-value"), + }), + }, + }, + want: want{ + changed: true, + }, + }, + "SecretUpdatedWithNewKey": { + reason: "Should update existing secret additively if a new key added", + args: args{ + client: resource.ClientApplicator{ + Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error { + if diff := cmp.Diff(fakeConnectionSecret(withData(map[string][]byte{ + "new-key": []byte("new-value"), + })), obj.(*corev1.Secret)); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + for _, fn := range option { + if err := fn(ctx, fakeConnectionSecret(withData(map[string][]byte{ + "existing-key": []byte("existing-value"), + })), obj); err != nil { + return err + } + } + return nil + }), + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: fakeSecretName, + Scope: fakeSecretNamespace, + }, + Data: store.KeyValues(map[string][]byte{ + "new-key": []byte("new-value"), + }), + }, + }, + want: want{ + changed: true, + }, + }, + "SecretCreatedWithData": { + reason: "Should create a secret with all key values with default type.", + args: args{ + client: resource.ClientApplicator{ + Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error { + if diff := cmp.Diff(fakeConnectionSecret(withData(fakeKV())), obj.(*corev1.Secret)); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + for _, fn := range option { + if err := fn(ctx, &corev1.Secret{}, obj); err != nil { + return err + } + } + return nil + }), + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: fakeSecretName, + Scope: fakeSecretNamespace, + }, + Data: store.KeyValues(fakeKV()), + }, + }, + want: want{ + changed: true, + }, + }, + "SecretCreatedWithDataAndMetadata": { + reason: "Should create a secret with all key values and provided metadata data.", + args: args{ + client: resource.ClientApplicator{ + Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error { + if diff := cmp.Diff(fakeConnectionSecret( + withData(fakeKV()), + withType(corev1.SecretTypeOpaque), + withLabels(fakeLabels()), + withAnnotations(fakeAnnotations())), obj.(*corev1.Secret)); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + for _, fn := range option { + if err := fn(ctx, &corev1.Secret{}, obj); err != nil { + return err + } + } + return nil + }), + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: fakeSecretName, + Scope: fakeSecretNamespace, + }, + Metadata: &v1.ConnectionSecretMetadata{ + Labels: map[string]string{ + "environment": "unit-test", + "reason": "testing", + }, + Annotations: map[string]string{ + "some-annotation-key": "some-annotation-value", + }, + Type: &secretTypeOpaque, + }, + Data: store.KeyValues(fakeKV()), + }, + }, + want: want{ + changed: true, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + ss := &SecretStore{ + client: tc.args.client, + defaultNamespace: tc.args.defaultNamespace, + } + changed, err := ss.WriteKeyValues(context.Background(), tc.args.secret, tc.args.wo...) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nss.WriteKeyValues(...): -want error, +got error:\n%s", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.changed, changed); diff != "" { + t.Errorf("\n%s\nss.WriteKeyValues(...): -want changed, +got changed:\n%s", tc.reason, diff) + } + }) + } +} + +func TestSecretStoreDeleteKeyValues(t *testing.T) { + type args struct { + client resource.ClientApplicator + defaultNamespace string + secret *store.Secret + + do []store.DeleteOption + } + type want struct { + err error + } + + cases := map[string]struct { + reason string + args + want + }{ + "CannotGetSecret": { + reason: "Should return a proper error when it fails to get secret.", + args: args{ + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(errBoom), + }, + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: fakeSecretName, + Scope: fakeSecretNamespace, + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errGetSecret), + }, + }, + "SecretUpdatedWithRemainingKeys": { + reason: "Should remove supplied keys from secret and update with remaining.", + args: args{ + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + *obj.(*corev1.Secret) = *fakeConnectionSecret(withData(fakeKV())) + return nil + }), + MockUpdate: func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + if diff := cmp.Diff(fakeConnectionSecret(withData(map[string][]byte{"key3": []byte("value3")})), obj.(*corev1.Secret)); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return nil + }, + }, + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: fakeSecretName, + Scope: fakeSecretNamespace, + }, + Data: store.KeyValues(map[string][]byte{ + "key1": []byte("value1"), + "key2": []byte("value2"), + }), + }, + }, + want: want{ + err: nil, + }, + }, + "CannotDeleteSecret": { + reason: "Should return a proper error when it fails to delete secret.", + args: args{ + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + *obj.(*corev1.Secret) = *fakeConnectionSecret() + return nil + }), + MockDelete: test.NewMockDeleteFn(errBoom), + }, + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: fakeSecretName, + Scope: fakeSecretNamespace, + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errDeleteSecret), + }, + }, + "SecretAlreadyDeleted": { + reason: "Should not return error if secret already deleted.", + args: args{ + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + return kerrors.NewNotFound(schema.GroupResource{}, "") + }), + }, + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: fakeSecretName, + Scope: fakeSecretNamespace, + }, + }, + }, + want: want{ + err: nil, + }, + }, + "FailedDeleteOption": { + reason: "Should return a proper error if provided delete option fails.", + args: args{ + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + *obj.(*corev1.Secret) = *fakeConnectionSecret(withData(fakeKV())) + return nil + }), + MockDelete: func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { + return nil + }, + }, + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: fakeSecretName, + Scope: fakeSecretNamespace, + }, + }, + do: []store.DeleteOption{ + func(ctx context.Context, secret *store.Secret) error { + return errBoom + }, + }, + }, + want: want{ + err: errBoom, + }, + }, + "SecretDeletedNoKVSupplied": { + reason: "Should delete the whole secret if no kv supplied as parameter.", + args: args{ + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + *obj.(*corev1.Secret) = *fakeConnectionSecret(withData(fakeKV())) + return nil + }), + MockDelete: func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { + return nil + }, + }, + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: fakeSecretName, + Scope: fakeSecretNamespace, + }, + }, + do: []store.DeleteOption{ + func(ctx context.Context, secret *store.Secret) error { + return nil + }, + }, + }, + want: want{ + err: nil, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + ss := &SecretStore{ + client: tc.args.client, + defaultNamespace: tc.args.defaultNamespace, + } + err := ss.DeleteKeyValues(context.Background(), tc.args.secret, tc.args.do...) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nss.DeleteKeyValues(...): -want error, +got error:\n%s", tc.reason, diff) + } + }) + } +} + +func TestNewSecretStore(t *testing.T) { + type args struct { + client resource.ClientApplicator + cfg v1.SecretStoreConfig + } + type want struct { + err error + } + + cases := map[string]struct { + reason string + args + want + }{ + "SuccessfulLocal": { + reason: "Should return no error after successfully building local Kubernetes secret store", + args: args{ + client: resource.ClientApplicator{}, + cfg: v1.SecretStoreConfig{ + Type: &storeTypeKubernetes, + DefaultScope: "test-ns", + }, + }, + want: want{ + err: nil, + }, + }, + "NoSecretWithRemoteKubeconfig": { + reason: "Should fail properly if configured kubeconfig secret does not exist", + args: args{ + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + return kerrors.NewNotFound(schema.GroupResource{}, "kube-conn") + }), + }, + }, + cfg: v1.SecretStoreConfig{ + Type: &storeTypeKubernetes, + DefaultScope: "test-ns", + Kubernetes: &v1.KubernetesSecretStoreConfig{ + Auth: v1.KubernetesAuthConfig{ + Source: v1.CredentialsSourceSecret, + CommonCredentialSelectors: v1.CommonCredentialSelectors{ + SecretRef: &v1.SecretKeySelector{ + SecretReference: v1.SecretReference{ + Name: "kube-conn", + Namespace: "test-ns", + }, + Key: "kubeconfig", + }, + }, + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errors.Wrap(errors.Wrap(kerrors.NewNotFound(schema.GroupResource{}, "kube-conn"), "cannot get credentials secret"), errExtractKubernetesAuthCreds), errBuildClient), + }, + }, + "InvalidRestConfigForRemote": { + reason: "Should fetch the configured kubeconfig and fail if it is not valid", + args: args{ + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + *obj.(*corev1.Secret) = corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-conn", + Namespace: "test-ns", + }, + Data: map[string][]byte{ + "kubeconfig": []byte(` +apiVersion: v1 +kind: Config +malformed +`), + }, + } + return nil + }), + }, + }, + cfg: v1.SecretStoreConfig{ + Type: &storeTypeKubernetes, + DefaultScope: "test-ns", + Kubernetes: &v1.KubernetesSecretStoreConfig{ + Auth: v1.KubernetesAuthConfig{ + Source: v1.CredentialsSourceSecret, + CommonCredentialSelectors: v1.CommonCredentialSelectors{ + SecretRef: &v1.SecretKeySelector{ + SecretReference: v1.SecretReference{ + Name: "kube-conn", + Namespace: "test-ns", + }, + Key: "kubeconfig", + }, + }, + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errors.Wrap(errors.New("yaml: line 5: could not find expected ':'"), errBuildRestConfig), errBuildClient), + }, + }, + "InvalidKubeconfigForRemote": { + reason: "Should fetch the configured kubeconfig and fail if it is not valid", + args: args{ + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + *obj.(*corev1.Secret) = corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-conn", + Namespace: "test-ns", + }, + Data: map[string][]byte{ + "kubeconfig": []byte(` +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: TEST + server: https://127.0.0.1:64695 + name: kind-kind +contexts: +- context: + cluster: kind-kind + namespace: crossplane-system + user: kind-kind + name: kind-kind +current-context: kind-kind +kind: Config +users: +- name: kind-kind + user: {} +`), + }, + } + return nil + }), + }, + }, + cfg: v1.SecretStoreConfig{ + Type: &storeTypeKubernetes, + DefaultScope: "test-ns", + Kubernetes: &v1.KubernetesSecretStoreConfig{ + Auth: v1.KubernetesAuthConfig{ + Source: v1.CredentialsSourceSecret, + CommonCredentialSelectors: v1.CommonCredentialSelectors{ + SecretRef: &v1.SecretKeySelector{ + SecretReference: v1.SecretReference{ + Name: "kube-conn", + Namespace: "test-ns", + }, + Key: "kubeconfig", + }, + }, + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errors.New("unable to load root certificates: unable to parse bytes as PEM block"), errBuildClient), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + _, err := NewSecretStore(context.Background(), tc.args.client, tc.args.cfg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nNewSecretStore(...): -want error, +got error:\n%s", tc.reason, diff) + } + }) + } +} + +type secretOption func(*corev1.Secret) + +func withType(t corev1.SecretType) secretOption { + return func(s *corev1.Secret) { + s.Type = t + } +} + +func withData(d map[string][]byte) secretOption { + return func(s *corev1.Secret) { + s.Data = d + } +} + +func withLabels(l map[string]string) secretOption { + return func(s *corev1.Secret) { + s.Labels = l + } +} + +func withAnnotations(a map[string]string) secretOption { + return func(s *corev1.Secret) { + s.Annotations = a + } +} +func fakeConnectionSecret(opts ...secretOption) *corev1.Secret { + s := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeSecretName, + Namespace: fakeSecretNamespace, + }, + Type: resource.SecretTypeConnection, + } + + for _, o := range opts { + o(s) + } + + return s +} diff --git a/pkg/connection/store/store.go b/pkg/connection/store/store.go new file mode 100644 index 000000000..068831493 --- /dev/null +++ b/pkg/connection/store/store.go @@ -0,0 +1,91 @@ +/* + 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 store + +import ( + "context" + + v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/resource" +) + +// SecretOwner owns a Secret. +type SecretOwner interface { + resource.Object + + resource.ConnectionDetailsPublisherTo +} + +// KeyValues is a map with sensitive values. +type KeyValues map[string][]byte + +// ScopedName is scoped name of a secret. +type ScopedName struct { + Name string + Scope string +} + +// A Secret is an entity representing a set of sensitive Key Values. +type Secret struct { + ScopedName + Metadata *v1.ConnectionSecretMetadata + Data KeyValues +} + +// NewSecret returns a new Secret owned by supplied SecretOwner and with +// supplied data. +func NewSecret(so SecretOwner, data KeyValues) *Secret { + if so.GetPublishConnectionDetailsTo() == nil { + return nil + } + p := so.GetPublishConnectionDetailsTo() + if p.Metadata == nil { + p.Metadata = &v1.ConnectionSecretMetadata{} + } + p.Metadata.SetOwnerUID(so.GetUID()) + return &Secret{ + ScopedName: ScopedName{ + Name: p.Name, + Scope: so.GetNamespace(), + }, + Metadata: p.Metadata, + Data: data, + } +} + +// GetOwner returns the UID of the owner of secret. +func (s *Secret) GetOwner() string { + if s.Metadata == nil { + return "" + } + return s.Metadata.GetOwnerUID() +} + +// GetLabels returns the labels of the secret. +func (s *Secret) GetLabels() map[string]string { + if s.Metadata == nil { + return nil + } + return s.Metadata.Labels +} + +// A WriteOption is called before writing the desired secret over the +// current object. +type WriteOption func(ctx context.Context, current, desired *Secret) error + +// An DeleteOption is called before deleting the secret. +type DeleteOption func(ctx context.Context, secret *Secret) error diff --git a/pkg/connection/store/vault/fake/mocks.go b/pkg/connection/store/vault/fake/mocks.go new file mode 100644 index 000000000..57bd14cec --- /dev/null +++ b/pkg/connection/store/vault/fake/mocks.go @@ -0,0 +1,43 @@ +/* + 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 fake + +import ( + "github.com/crossplane/crossplane-runtime/pkg/connection/store/vault/kv" +) + +// KVClient is a fake KVClient. +type KVClient struct { + GetFn func(path string, secret *kv.Secret) error + ApplyFn func(path string, secret *kv.Secret, ao ...kv.ApplyOption) error + DeleteFn func(path string) error +} + +// Get fetches a secret at a given path. +func (k *KVClient) Get(path string, secret *kv.Secret) error { + return k.GetFn(path, secret) +} + +// Apply creates or updates a secret at a given path. +func (k *KVClient) Apply(path string, secret *kv.Secret, ao ...kv.ApplyOption) error { + return k.ApplyFn(path, secret, ao...) +} + +// Delete deletes a secret at a given path. +func (k *KVClient) Delete(path string) error { + return k.DeleteFn(path) +} diff --git a/pkg/connection/store/vault/kv/fake/mocks.go b/pkg/connection/store/vault/kv/fake/mocks.go new file mode 100644 index 000000000..87b9e3b95 --- /dev/null +++ b/pkg/connection/store/vault/kv/fake/mocks.go @@ -0,0 +1,43 @@ +/* + 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 fake + +import ( + "github.com/hashicorp/vault/api" +) + +// LogicalClient is a fake LogicalClient +type LogicalClient struct { + ReadFn func(path string) (*api.Secret, error) + WriteFn func(path string, data map[string]interface{}) (*api.Secret, error) + DeleteFn func(path string) (*api.Secret, error) +} + +// Read reads secret at the given path. +func (l *LogicalClient) Read(path string) (*api.Secret, error) { + return l.ReadFn(path) +} + +// Write writes data to the given path. +func (l *LogicalClient) Write(path string, data map[string]interface{}) (*api.Secret, error) { + return l.WriteFn(path, data) +} + +// Delete deletes secret at the given path. +func (l *LogicalClient) Delete(path string) (*api.Secret, error) { + return l.DeleteFn(path) +} diff --git a/pkg/connection/store/vault/kv/secret.go b/pkg/connection/store/vault/kv/secret.go new file mode 100644 index 000000000..a221aaf1a --- /dev/null +++ b/pkg/connection/store/vault/kv/secret.go @@ -0,0 +1,99 @@ +/* + 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 kv + +import ( + "encoding/json" + + "github.com/hashicorp/vault/api" + + "github.com/crossplane/crossplane-runtime/pkg/resource" +) + +const ( + errGet = "cannot get secret" + errDelete = "cannot delete secret" + errRead = "cannot read secret" + errWriteData = "cannot write secret Data" + errUpdateNotAllowed = "update not allowed" + + // ErrNotFound is the error returned when secret does not exist. + ErrNotFound = "secret not found" +) + +// LogicalClient is a client to perform logical backend operations on Vault. +type LogicalClient interface { + Read(path string) (*api.Secret, error) + Write(path string, data map[string]interface{}) (*api.Secret, error) + Delete(path string) (*api.Secret, error) +} + +// Secret is a Vault KV secret. +type Secret struct { + CustomMeta map[string]string + Data map[string]string + version json.Number +} + +// NewSecret returns a new Secret. +func NewSecret(data map[string]string, meta map[string]string) *Secret { + return &Secret{ + Data: data, + CustomMeta: meta, + } +} + +// AddData adds supplied key value as data. +func (kv *Secret) AddData(key string, val string) { + if kv.Data == nil { + kv.Data = map[string]string{} + } + kv.Data[key] = val +} + +// AddMetadata adds supplied key value as metadata. +func (kv *Secret) AddMetadata(key string, val string) { + if kv.CustomMeta == nil { + kv.CustomMeta = map[string]string{} + } + kv.CustomMeta[key] = val +} + +// An ApplyOption is called before patching the current secret to match the +// desired secret. ApplyOptions are not called if no current object exists. +type ApplyOption func(current, desired *Secret) error + +// AllowUpdateIf will only update the current object if the supplied fn returns +// true. An error that satisfies IsNotAllowed will be returned if the supplied +// function returns false. Creation of a desired object that does not currently +// exist is always allowed. +func AllowUpdateIf(fn func(current, desired *Secret) bool) ApplyOption { + return func(current, desired *Secret) error { + if fn(current, desired) { + return nil + } + return resource.NewNotAllowed(errUpdateNotAllowed) + } +} + +// IsNotFound returns whether given error is a "Not Found" error or not. +func IsNotFound(err error) bool { + if err == nil { + return false + } + return err.Error() == ErrNotFound +} diff --git a/pkg/connection/store/vault/kv/v1.go b/pkg/connection/store/vault/kv/v1.go new file mode 100644 index 000000000..e24dc8609 --- /dev/null +++ b/pkg/connection/store/vault/kv/v1.go @@ -0,0 +1,133 @@ +/* + Copyright 2022 The Crossplane Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package kv + +import ( + "path/filepath" + "strings" + + "github.com/hashicorp/vault/api" + + "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/resource" +) + +// We use this prefix to store metadata of v1 secrets as there is no dedicated +// metadata. Considering a connection key cannot contain ":" (since it is not +// in the set of allowed chars for a k8s secret key), it is safe to assume +// there is no actual connection data starting with this prefix. +const metadataPrefix = "metadata:" + +// V1Client is a Vault KV V1 Secrets Engine client. +// https://www.vaultproject.io/api-docs/secret/kv/kv-v1 +type V1Client struct { + client LogicalClient + mountPath string +} + +// NewV1Client returns a new V1Client. +func NewV1Client(logical LogicalClient, mountPath string) *V1Client { + kv := &V1Client{ + client: logical, + mountPath: mountPath, + } + + return kv +} + +// Get returns a Secret at a given path. +func (c *V1Client) Get(path string, secret *Secret) error { + s, err := c.client.Read(filepath.Join(c.mountPath, path)) + if err != nil { + return errors.Wrap(err, errRead) + } + if s == nil { + return errors.New(ErrNotFound) + } + return c.parseAsSecret(s, secret) +} + +// Apply applies given Secret at path by patching its Data and setting +// provided custom metadata. +func (c *V1Client) Apply(path string, secret *Secret, ao ...ApplyOption) error { + existing := &Secret{} + err := c.Get(path, existing) + + if resource.Ignore(IsNotFound, err) != nil { + return errors.Wrap(err, errGet) + } + if !IsNotFound(err) { + for _, o := range ao { + if err = o(existing, secret); err != nil { + return err + } + } + } + + dp, changed := payloadV1(existing, secret) + if !changed { + return nil + } + _, err = c.client.Write(filepath.Join(c.mountPath, path), dp) + return errors.Wrap(err, errWriteData) + +} + +// Delete deletes Secret at the given path. +func (c *V1Client) Delete(path string) error { + _, err := c.client.Delete(filepath.Join(c.mountPath, path)) + return errors.Wrap(err, errDelete) +} + +func (c *V1Client) parseAsSecret(s *api.Secret, kv *Secret) error { + for key, val := range s.Data { + if sVal, ok := val.(string); ok { + if strings.HasPrefix(key, metadataPrefix) { + kv.AddMetadata(strings.TrimPrefix(key, metadataPrefix), sVal) + continue + } + kv.AddData(key, sVal) + } + } + return nil +} + +func payloadV1(existing, new *Secret) (map[string]interface{}, bool) { + payload := make(map[string]interface{}, len(existing.Data)+len(new.Data)) + for k, v := range existing.Data { + // Only transfer existing data, metadata updates are not additive. + if !strings.HasPrefix(k, metadataPrefix) { + payload[k] = v + } + } + changed := false + for k, v := range new.Data { + if ev, ok := existing.Data[k]; !ok || ev != v { + changed = true + payload[k] = v + } + } + for k, v := range new.CustomMeta { + // kv secret engine v1 does not have metadata. So, we store them as data + // by prefixing with "metadata:" + if val, ok := existing.CustomMeta[k]; !ok && val != v { + changed = true + } + payload[metadataPrefix+k] = v + } + return payload, changed +} diff --git a/pkg/connection/store/vault/kv/v1_test.go b/pkg/connection/store/vault/kv/v1_test.go new file mode 100644 index 000000000..5986c6803 --- /dev/null +++ b/pkg/connection/store/vault/kv/v1_test.go @@ -0,0 +1,491 @@ +/* + 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 kv + +import ( + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/vault/api" + + "github.com/crossplane/crossplane-runtime/pkg/connection/store/vault/kv/fake" + "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/resource" + "github.com/crossplane/crossplane-runtime/pkg/test" +) + +func TestV1ClientGet(t *testing.T) { + type args struct { + client LogicalClient + path string + } + type want struct { + err error + out *Secret + } + cases := map[string]struct { + reason string + args + want + }{ + "ErrorWhileGettingSecret": { + reason: "Should return a proper error if getting secret failed.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return nil, errBoom + }, + }, + path: secretName, + }, + want: want{ + err: errors.Wrap(errBoom, errRead), + out: NewSecret(nil, nil), + }, + }, + "SecretNotFound": { + reason: "Should return a notFound error if secret does not exist.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + // Vault logical client returns both error and secret as + // nil if secret does not exist. + return nil, nil + }, + }, + path: secretName, + }, + want: want{ + err: errors.New(ErrNotFound), + out: NewSecret(nil, nil), + }, + }, + "SuccessfulGet": { + reason: "Should successfully return secret from v1 KV engine.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + if diff := cmp.Diff(filepath.Join(mountPath, secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return &api.Secret{ + Data: map[string]interface{}{ + "foo": "bar", + metadataPrefix + "owner": "jdoe", + metadataPrefix + "mission_critical": "false", + }, + }, nil + }, + }, + path: secretName, + }, + want: want{ + out: NewSecret(map[string]string{ + "foo": "bar", + }, map[string]string{ + "owner": "jdoe", + "mission_critical": "false", + }), + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + k := NewV1Client(tc.args.client, mountPath) + + s := Secret{} + err := k.Get(tc.args.path, &s) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nv1Client.Get(...): -want error, +got error:\n%s", tc.reason, diff) + } + + if diff := cmp.Diff(tc.want.out, &s, cmpopts.IgnoreUnexported(Secret{})); diff != "" { + t.Errorf("\n%s\nv1Client.Get(...): -want, +got:\n%s", tc.reason, diff) + } + }) + } +} + +func TestV1ClientApply(t *testing.T) { + type args struct { + client LogicalClient + in *Secret + path string + + ao []ApplyOption + } + type want struct { + err error + } + cases := map[string]struct { + reason string + args + want + }{ + "ErrorWhileReadingSecret": { + reason: "Should return a proper error if reading secret failed.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return nil, errBoom + }, + }, + path: secretName, + }, + want: want{ + err: errors.Wrap(errors.Wrap(errBoom, errRead), errGet), + }, + }, + "ErrorWhileWritingData": { + reason: "Should return a proper error if writing secret failed.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return &api.Secret{ + Data: map[string]interface{}{ + "key1": "val1", + "key2": "val2", + }, + }, nil + }, + WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) { + return nil, errBoom + }, + }, + path: secretName, + in: NewSecret(map[string]string{ + "key1": "val1updated", + "key3": "val3", + }, nil), + }, + want: want{ + err: errors.Wrap(errBoom, errWriteData), + }, + }, + "AlreadyUpToDate": { + reason: "Should not perform a write if a v1 secret is already up to date.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return &api.Secret{ + Data: map[string]interface{}{ + "foo": "bar", + metadataPrefix + "owner": "jdoe", + metadataPrefix + "mission_critical": "false", + }, + }, nil + }, + WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) { + return nil, errors.New("no write operation expected") + }, + }, + path: secretName, + in: NewSecret(map[string]string{ + "foo": "bar", + }, map[string]string{ + "owner": "jdoe", + "mission_critical": "false", + }), + }, + want: want{ + err: nil, + }, + }, + "SuccessfulCreate": { + reason: "Should successfully create with new data if secret does not exists.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + // Vault logical client returns both error and secret as + // nil if secret does not exist. + return nil, nil + }, + WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) { + if diff := cmp.Diff(filepath.Join(mountPath, secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + if diff := cmp.Diff(map[string]interface{}{ + "key1": "val1", + "key2": "val2", + }, data); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return nil, nil + }, + }, + path: secretName, + in: NewSecret(map[string]string{ + "key1": "val1", + "key2": "val2", + }, nil), + }, + want: want{ + err: nil, + }, + }, + "UpdateNotAllowed": { + reason: "Should return not allowed error if update is not allowed.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return &api.Secret{ + Data: map[string]interface{}{ + "key1": "val1", + "key2": "val2", + }, + }, nil + }, + WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) { + if diff := cmp.Diff(filepath.Join(mountPath, secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + if diff := cmp.Diff(map[string]interface{}{ + "key1": "val1updated", + "key2": "val2", + "key3": "val3", + }, data); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return nil, nil + }, + }, + path: secretName, + in: NewSecret(map[string]string{ + "key1": "val1updated", + "key3": "val3", + }, nil), + ao: []ApplyOption{ + AllowUpdateIf(func(current, desired *Secret) bool { + return false + }), + }, + }, + want: want{ + err: resource.NewNotAllowed(errUpdateNotAllowed), + }, + }, + "SuccessfulUpdate": { + reason: "Should successfully update by appending new data to existing ones.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return &api.Secret{ + Data: map[string]interface{}{ + "key1": "val1", + "key2": "val2", + }, + }, nil + }, + WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) { + if diff := cmp.Diff(filepath.Join(mountPath, secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + if diff := cmp.Diff(map[string]interface{}{ + "key1": "val1updated", + "key2": "val2", + "key3": "val3", + }, data); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return nil, nil + }, + }, + path: secretName, + in: NewSecret(map[string]string{ + "key1": "val1updated", + "key3": "val3", + }, nil), + ao: []ApplyOption{ + AllowUpdateIf(func(current, desired *Secret) bool { + return true + }), + }, + }, + want: want{ + err: nil, + }, + }, + "SuccessfulAddMetadata": { + reason: "Should successfully add new metadata.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return &api.Secret{ + Data: map[string]interface{}{ + "key1": "val1", + "key2": "val2", + }, + }, nil + }, + WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) { + if diff := cmp.Diff(filepath.Join(mountPath, secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + if diff := cmp.Diff(map[string]interface{}{ + "key1": "val1", + "key2": "val2", + metadataPrefix + "foo": "bar", + metadataPrefix + "baz": "qux", + }, data); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return nil, nil + }, + }, + path: secretName, + in: NewSecret(map[string]string{ + "key1": "val1", + "key2": "val2", + }, map[string]string{ + "foo": "bar", + "baz": "qux", + }), + }, + want: want{ + err: nil, + }, + }, + "SuccessfulUpdateMetadata": { + reason: "Should successfully update metadata by overriding the existing ones.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return &api.Secret{ + Data: map[string]interface{}{ + "key1": "val1", + "key2": "val2", + metadataPrefix + "old": "meta", + }, + }, nil + }, + WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) { + if diff := cmp.Diff(filepath.Join(mountPath, secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + if diff := cmp.Diff(map[string]interface{}{ + "key1": "val1", + "key2": "val2", + metadataPrefix + "old": "meta", + metadataPrefix + "foo": "bar", + }, data); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return nil, nil + }, + }, + path: secretName, + in: NewSecret(map[string]string{ + "key1": "val1", + "key2": "val2", + }, map[string]string{ + "old": "meta", + "foo": "bar", + }), + }, + want: want{ + err: nil, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + k := NewV1Client(tc.args.client, mountPath) + + err := k.Apply(tc.args.path, tc.args.in, tc.args.ao...) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nv1Client.Apply(...): -want error, +got error:\n%s", tc.reason, diff) + } + }) + } +} + +func TestV1ClientDelete(t *testing.T) { + type args struct { + client LogicalClient + path string + } + type want struct { + err error + } + cases := map[string]struct { + reason string + args + want + }{ + "ErrorWhileDeletingSecret": { + reason: "Should return a proper error if deleting secret failed.", + args: args{ + client: &fake.LogicalClient{ + DeleteFn: func(path string) (*api.Secret, error) { + return nil, errBoom + }, + }, + path: secretName, + }, + want: want{ + err: errors.Wrap(errBoom, errDelete), + }, + }, + "SecretAlreadyDeleted": { + reason: "Should return success if secret already deleted.", + args: args{ + client: &fake.LogicalClient{ + DeleteFn: func(path string) (*api.Secret, error) { + // Vault logical client returns both error and secret as + // nil if secret does not exist. + return nil, nil + }, + }, + path: secretName, + }, + want: want{ + err: nil, + }, + }, + "SuccessfulDelete": { + reason: "Should return no error after successful deletion of a v1 secret.", + args: args{ + client: &fake.LogicalClient{ + DeleteFn: func(path string) (*api.Secret, error) { + if diff := cmp.Diff(filepath.Join(mountPath, secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return &api.Secret{ + Data: map[string]interface{}{ + "foo": "bar", + }, + }, nil + }, + }, + path: secretName, + }, + want: want{}, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + k := NewV1Client(tc.args.client, mountPath) + + err := k.Delete(tc.args.path) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nv1Client.Get(...): -want error, +got error:\n%s", tc.reason, diff) + } + }) + } +} diff --git a/pkg/connection/store/vault/kv/v2.go b/pkg/connection/store/vault/kv/v2.go new file mode 100644 index 000000000..759b44e0e --- /dev/null +++ b/pkg/connection/store/vault/kv/v2.go @@ -0,0 +1,219 @@ +/* + 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 kv + +import ( + "encoding/json" + "path/filepath" + + "github.com/crossplane/crossplane-runtime/pkg/fieldpath" + + "github.com/hashicorp/vault/api" + + "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/resource" +) + +const ( + errWriteMetadata = "cannot write secret metadata Data" +) + +// V2Client is a Vault KV V2 Secrets Engine client. +// https://www.vaultproject.io/api/secret/kv/kv-v2 +type V2Client struct { + client LogicalClient + mountPath string +} + +// NewV2Client returns a new V2Client. +func NewV2Client(logical LogicalClient, mountPath string) *V2Client { + kv := &V2Client{ + client: logical, + mountPath: mountPath, + } + + return kv +} + +// Get returns a Secret at a given path. +func (c *V2Client) Get(path string, secret *Secret) error { + s, err := c.client.Read(c.dataPath(path)) + if err != nil { + return errors.Wrap(err, errRead) + } + if s == nil { + return errors.New(ErrNotFound) + } + return c.parseAsKVSecret(s, secret) +} + +// Apply applies given Secret at path by patching its Data and setting +// provided custom metadata. +func (c *V2Client) Apply(path string, secret *Secret, ao ...ApplyOption) error { + existing := &Secret{} + err := c.Get(path, existing) + + if resource.Ignore(IsNotFound, err) != nil { + return errors.Wrap(err, errGet) + } + if !IsNotFound(err) { + for _, o := range ao { + if err = o(existing, secret); err != nil { + return err + } + } + } + + // We write metadata first to ensure we set ownership (with the label) of + // the secret before writing any data. This is to prevent situations where + // secret create with some data but owner not set. + mp, changed := metadataPayload(existing.CustomMeta, secret.CustomMeta) + if changed { + if _, err := c.client.Write(c.metadataPath(path), mp); err != nil { + return errors.Wrap(err, errWriteMetadata) + } + } + + dp, changed := dataPayload(existing, secret) + if changed { + if _, err := c.client.Write(c.dataPath(path), dp); err != nil { + return errors.Wrap(err, errWriteData) + } + } + + return nil +} + +// Delete deletes Secret at the given path. +func (c *V2Client) Delete(path string) error { + // Note(turkenh): With V2Client, we need to delete metadata and all versions: + // https://www.vaultproject.io/api-docs/secret/kv/kv-v2#delete-metadata-and-all-versions + _, err := c.client.Delete(c.metadataPath(path)) + return errors.Wrap(err, errDelete) +} + +func dataPayload(existing, new *Secret) (map[string]interface{}, bool) { + data := make(map[string]string, len(existing.Data)+len(new.Data)) + for k, v := range existing.Data { + data[k] = v + } + changed := false + for k, v := range new.Data { + if ev, ok := existing.Data[k]; !ok || ev != v { + changed = true + data[k] = v + } + } + ver := json.Number("0") + if existing.version != "" { + ver = existing.version + } + return map[string]interface{}{ + "options": map[string]interface{}{ + "cas": ver, + }, + "data": data, + }, changed +} + +func metadataPayload(existing, new map[string]string) (map[string]interface{}, bool) { + payload := map[string]interface{}{ + "custom_metadata": new, + } + if len(existing) != len(new) { + return payload, true + } + for k, v := range new { + if ev, ok := existing[k]; !ok || ev != v { + return payload, true + } + } + return payload, false +} + +func (c *V2Client) parseAsKVSecret(s *api.Secret, kv *Secret) error { + // Note(turkenh): kv v2 secrets contains another "data" and "metadata" + // blocks inside the top level generic "Data" field. + // https://www.vaultproject.io/api/secret/kv/kv-v2#sample-response-1 + paved := fieldpath.Pave(s.Data) + if err := parseSecretData(paved, kv); err != nil { + return err + } + if err := parseSecretMeta(paved, kv); err != nil { + return err + } + return nil +} + +func parseSecretData(payload *fieldpath.Paved, kv *Secret) error { + sData := map[string]interface{}{} + err := payload.GetValueInto("data", &sData) + if fieldpath.IsNotFound(err) { + return nil + } + if err != nil { + return err + } + + kv.Data = make(map[string]string, len(sData)) + for key, val := range sData { + if sVal, ok := val.(string); ok { + kv.Data[key] = sVal + } + } + return nil +} + +func parseSecretMeta(payload *fieldpath.Paved, kv *Secret) error { + sMeta := map[string]interface{}{} + err := payload.GetValueInto("metadata", &sMeta) + if fieldpath.IsNotFound(err) { + return nil + } + if err != nil { + return err + } + + pavedMeta := fieldpath.Pave(sMeta) + if err = pavedMeta.GetValueInto("version", &kv.version); resource.Ignore(fieldpath.IsNotFound, err) != nil { + return err + } + + customMeta := map[string]interface{}{} + err = pavedMeta.GetValueInto("custom_metadata", &customMeta) + if fieldpath.IsNotFound(err) { + return nil + } + if err != nil { + return err + } + kv.CustomMeta = make(map[string]string, len(customMeta)) + for key, val := range customMeta { + if sVal, ok := val.(string); ok { + kv.CustomMeta[key] = sVal + } + } + return nil +} + +func (c *V2Client) dataPath(secretPath string) string { + return filepath.Join(c.mountPath, "data", secretPath) +} + +func (c *V2Client) metadataPath(secretPath string) string { + return filepath.Join(c.mountPath, "metadata", secretPath) +} diff --git a/pkg/connection/store/vault/kv/v2_test.go b/pkg/connection/store/vault/kv/v2_test.go new file mode 100644 index 000000000..3f0f05cfd --- /dev/null +++ b/pkg/connection/store/vault/kv/v2_test.go @@ -0,0 +1,682 @@ +/* + 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 kv + +import ( + "encoding/json" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/vault/api" + + "github.com/crossplane/crossplane-runtime/pkg/connection/store/vault/kv/fake" + "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/resource" + "github.com/crossplane/crossplane-runtime/pkg/test" +) + +const ( + mountPath = "test-secrets/" + + secretName = "conn-unittests" +) + +var ( + errBoom = errors.New("boom") +) + +func TestV2ClientGet(t *testing.T) { + type args struct { + client LogicalClient + path string + } + type want struct { + err error + out *Secret + } + cases := map[string]struct { + reason string + args + want + }{ + "ErrorWhileGettingSecret": { + reason: "Should return a proper error if getting secret failed.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return nil, errBoom + }, + }, + path: secretName, + }, + want: want{ + err: errors.Wrap(errBoom, errRead), + out: NewSecret(nil, nil), + }, + }, + "SecretNotFound": { + reason: "Should return a notFound error if secret does not exist.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + // Vault logical client returns both error and secret as + // nil if secret does not exist. + return nil, nil + }, + }, + path: secretName, + }, + want: want{ + err: errors.New(ErrNotFound), + out: NewSecret(nil, nil), + }, + }, + "SuccessfulGetNoData": { + reason: "Should successfully return secret from v2 KV engine even it only contains metadata.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + if diff := cmp.Diff(filepath.Join(mountPath, "data", secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return &api.Secret{ + // Using sample response here: + // https://www.vaultproject.io/api/secret/kv/kv-v2#sample-response-1 + Data: map[string]interface{}{ + "metadata": map[string]interface{}{ + "created_time": "2018-03-22T02:24:06.945319214Z", + "custom_metadata": map[string]interface{}{ + "owner": "jdoe", + "mission_critical": "false", + }, + "deletion_time": "", + "destroyed": false, + }, + }, + }, nil + }, + }, + path: secretName, + }, + want: want{ + out: NewSecret(nil, map[string]string{ + "owner": "jdoe", + "mission_critical": "false", + }), + }, + }, + "SuccessfulGetNoMetadata": { + reason: "Should successfully return secret from v2 KV engine even it only contains data.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + if diff := cmp.Diff(filepath.Join(mountPath, "data", secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return &api.Secret{ + // Using sample response here: + // https://www.vaultproject.io/api/secret/kv/kv-v2#sample-response-1 + Data: map[string]interface{}{ + "data": map[string]interface{}{ + "foo": "bar", + }, + }, + }, nil + }, + }, + path: secretName, + }, + want: want{ + out: NewSecret(map[string]string{ + "foo": "bar", + }, nil), + }, + }, + "SuccessfulGet": { + reason: "Should successfully return secret from v2 KV engine.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + if diff := cmp.Diff(filepath.Join(mountPath, "data", secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return &api.Secret{ + // Using sample response here: + // https://www.vaultproject.io/api/secret/kv/kv-v2#sample-response-1 + Data: map[string]interface{}{ + "data": map[string]interface{}{ + "foo": "bar", + }, + "metadata": map[string]interface{}{ + "created_time": "2018-03-22T02:24:06.945319214Z", + "custom_metadata": map[string]interface{}{ + "owner": "jdoe", + "mission_critical": "false", + }, + "deletion_time": "", + "destroyed": false, + "version": 2, + }, + }, + }, nil + }, + }, + path: secretName, + }, + want: want{ + out: NewSecret(map[string]string{ + "foo": "bar", + }, map[string]string{ + "owner": "jdoe", + "mission_critical": "false", + }), + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + k := NewV2Client(tc.args.client, mountPath) + + s := Secret{} + err := k.Get(tc.args.path, &s) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nv2Client.Get(...): -want error, +got error:\n%s", tc.reason, diff) + } + + if diff := cmp.Diff(tc.want.out, &s, cmpopts.IgnoreUnexported(Secret{})); diff != "" { + t.Errorf("\n%s\nv2Client.Get(...): -want, +got:\n%s", tc.reason, diff) + } + }) + } +} + +func TestV2ClientApply(t *testing.T) { + type args struct { + client LogicalClient + in *Secret + path string + + ao []ApplyOption + } + type want struct { + err error + } + cases := map[string]struct { + reason string + args + want + }{ + "ErrorWhileReadingSecret": { + reason: "Should return a proper error if reading secret failed.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return nil, errBoom + }, + }, + path: secretName, + }, + want: want{ + err: errors.Wrap(errors.Wrap(errBoom, errRead), errGet), + }, + }, + "ErrorWhileWritingData": { + reason: "Should return a proper error if writing secret failed.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return &api.Secret{ + Data: map[string]interface{}{ + "data": map[string]string{ + "key1": "val1", + "key2": "val2", + }, + "metadata": map[string]interface{}{ + "custom_metadata": map[string]interface{}{ + "foo": "bar", + "baz": "qux", + }, + "version": json.Number("2"), + }, + }, + }, nil + }, + WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) { + return nil, errBoom + }, + }, + path: secretName, + in: NewSecret(map[string]string{ + "key1": "val1updated", + "key3": "val3", + }, map[string]string{ + "foo": "bar", + "baz": "qux", + }), + }, + want: want{ + err: errors.Wrap(errBoom, errWriteData), + }, + }, + "ErrorWhileWritingMetadata": { + reason: "Should return a proper error if writing secret metadata failed.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return &api.Secret{ + Data: map[string]interface{}{ + "data": map[string]string{ + "key1": "val1", + "key2": "val2", + }, + "metadata": map[string]interface{}{ + "custom_metadata": map[string]interface{}{ + "foo": "bar", + }, + "version": json.Number("2"), + }, + }, + }, nil + }, + WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) { + return nil, errBoom + }, + }, + path: secretName, + in: NewSecret(map[string]string{ + "key1": "val1", + "key2": "val2", + }, map[string]string{ + "foo": "baz", + }), + }, + want: want{ + err: errors.Wrap(errBoom, errWriteMetadata), + }, + }, + "AlreadyUpToDate": { + reason: "Should not perform a write if a v2 secret is already up to date.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return &api.Secret{ + // Using sample response here: + // https://www.vaultproject.io/api/secret/kv/kv-v2#sample-response-1 + Data: map[string]interface{}{ + "data": map[string]interface{}{ + "foo": "bar", + }, + "metadata": map[string]interface{}{ + "created_time": "2018-03-22T02:24:06.945319214Z", + "custom_metadata": map[string]interface{}{ + "owner": "jdoe", + "mission_critical": "false", + }, + "deletion_time": "", + "destroyed": false, + "version": 2, + }, + }, + }, nil + }, + WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) { + return nil, errors.New("no write operation expected") + }, + }, + path: secretName, + in: NewSecret(map[string]string{ + "foo": "bar", + }, map[string]string{ + "owner": "jdoe", + "mission_critical": "false", + }), + }, + want: want{ + err: nil, + }, + }, + "SuccessfulCreate": { + reason: "Should successfully create with new data if secret does not exists.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + // Vault logical client returns both error and secret as + // nil if secret does not exist. + return nil, nil + }, + WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) { + if diff := cmp.Diff(filepath.Join(mountPath, "data", secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + if diff := cmp.Diff(map[string]interface{}{ + "data": map[string]string{ + "key1": "val1", + "key2": "val2", + }, + "options": map[string]interface{}{ + "cas": json.Number("0"), + }, + }, data); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return nil, nil + }, + }, + path: secretName, + in: NewSecret(map[string]string{ + "key1": "val1", + "key2": "val2", + }, nil), + }, + want: want{ + err: nil, + }, + }, + "UpdateNotAllowed": { + reason: "Should return not allowed error if update is not allowed.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return &api.Secret{ + Data: map[string]interface{}{ + "data": map[string]interface{}{ + "key1": "val1", + "key2": "val2", + }, + "metadata": map[string]interface{}{ + "custom_metadata": map[string]interface{}{ + "foo": "bar", + "baz": "qux", + }, + "version": json.Number("2"), + }, + }, + }, nil + }, + WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) { + if diff := cmp.Diff(filepath.Join(mountPath, "data", secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + if diff := cmp.Diff(map[string]interface{}{ + "data": map[string]string{ + "key1": "val1updated", + "key2": "val2", + "key3": "val3", + }, + "options": map[string]interface{}{ + "cas": json.Number("2"), + }, + }, data); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return nil, nil + }, + }, + path: secretName, + in: NewSecret(map[string]string{ + "key1": "val1updated", + "key3": "val3", + }, map[string]string{ + "foo": "bar", + "baz": "qux", + }), + ao: []ApplyOption{ + AllowUpdateIf(func(current, desired *Secret) bool { + return false + }), + }, + }, + want: want{ + err: resource.NewNotAllowed(errUpdateNotAllowed), + }, + }, + "SuccessfulUpdateData": { + reason: "Should successfully update by appending new data to existing ones.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return &api.Secret{ + Data: map[string]interface{}{ + "data": map[string]interface{}{ + "key1": "val1", + "key2": "val2", + }, + "metadata": map[string]interface{}{ + "custom_metadata": map[string]interface{}{ + "foo": "bar", + "baz": "qux", + }, + "version": json.Number("2"), + }, + }, + }, nil + }, + WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) { + if diff := cmp.Diff(filepath.Join(mountPath, "data", secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + if diff := cmp.Diff(map[string]interface{}{ + "data": map[string]string{ + "key1": "val1updated", + "key2": "val2", + "key3": "val3", + }, + "options": map[string]interface{}{ + "cas": json.Number("2"), + }, + }, data); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return nil, nil + }, + }, + path: secretName, + in: NewSecret(map[string]string{ + "key1": "val1updated", + "key3": "val3", + }, map[string]string{ + "foo": "bar", + "baz": "qux", + }), + ao: []ApplyOption{ + AllowUpdateIf(func(current, desired *Secret) bool { + return true + }), + }, + }, + want: want{ + err: nil, + }, + }, + "SuccessfulAddMetadata": { + reason: "Should successfully add new metadata.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return &api.Secret{ + Data: map[string]interface{}{ + "data": map[string]interface{}{ + "key1": "val1", + "key2": "val2", + }, + "metadata": map[string]interface{}{ + "version": json.Number("2"), + }, + }, + }, nil + }, + WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) { + if diff := cmp.Diff(filepath.Join(mountPath, "metadata", secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + if diff := cmp.Diff(map[string]interface{}{ + "custom_metadata": map[string]string{ + "foo": "bar", + "baz": "qux", + }, + }, data); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return nil, nil + }, + }, + path: secretName, + in: NewSecret(map[string]string{ + "key1": "val1", + "key2": "val2", + }, map[string]string{ + "foo": "bar", + "baz": "qux", + }), + }, + want: want{ + err: nil, + }, + }, + "SuccessfulUpdateMetadata": { + reason: "Should successfully update metadata by overriding the existing ones.", + args: args{ + client: &fake.LogicalClient{ + ReadFn: func(path string) (*api.Secret, error) { + return &api.Secret{ + Data: map[string]interface{}{ + "data": map[string]interface{}{ + "key1": "val1", + "key2": "val2", + }, + "metadata": map[string]interface{}{ + "custom_metadata": map[string]interface{}{ + "old": "meta", + }, + "version": json.Number("2"), + }, + }, + }, nil + }, + WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) { + if diff := cmp.Diff(filepath.Join(mountPath, "metadata", secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + if diff := cmp.Diff(map[string]interface{}{ + "custom_metadata": map[string]string{ + "foo": "bar", + "baz": "qux", + }, + }, data); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return nil, nil + }, + }, + path: secretName, + in: NewSecret(map[string]string{ + "key1": "val1", + "key2": "val2", + }, map[string]string{ + "foo": "bar", + "baz": "qux", + }), + }, + want: want{ + err: nil, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + k := NewV2Client(tc.args.client, mountPath) + + err := k.Apply(tc.args.path, tc.args.in, tc.args.ao...) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nv2Client.Apply(...): -want error, +got error:\n%s", tc.reason, diff) + } + }) + } +} + +func TestV2ClientDelete(t *testing.T) { + type args struct { + client LogicalClient + path string + } + type want struct { + err error + } + cases := map[string]struct { + reason string + args + want + }{ + "ErrorWhileDeletingSecret": { + reason: "Should return a proper error if deleting secret failed.", + args: args{ + client: &fake.LogicalClient{ + DeleteFn: func(path string) (*api.Secret, error) { + return nil, errBoom + }, + }, + path: secretName, + }, + want: want{ + err: errors.Wrap(errBoom, errDelete), + }, + }, + "SecretAlreadyDeleted": { + reason: "Should return success if secret already deleted.", + args: args{ + client: &fake.LogicalClient{ + DeleteFn: func(path string) (*api.Secret, error) { + // Vault logical client returns both error and secret as + // nil if secret does not exist. + return nil, nil + }, + }, + path: secretName, + }, + want: want{ + err: nil, + }, + }, + "SuccessfulDelete": { + reason: "Should return no error after successful deletion of a v2 secret.", + args: args{ + client: &fake.LogicalClient{ + DeleteFn: func(path string) (*api.Secret, error) { + if diff := cmp.Diff(filepath.Join(mountPath, "metadata", secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return nil, nil + }, + }, + path: secretName, + }, + want: want{}, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + k := NewV2Client(tc.args.client, mountPath) + + err := k.Delete(tc.args.path) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nv2Client.Get(...): -want error, +got error:\n%s", tc.reason, diff) + } + }) + } +} diff --git a/pkg/connection/store/vault/store.go b/pkg/connection/store/vault/store.go new file mode 100644 index 000000000..5902f2d38 --- /dev/null +++ b/pkg/connection/store/vault/store.go @@ -0,0 +1,247 @@ +/* +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 vault + +import ( + "context" + "crypto/x509" + "net/http" + "path/filepath" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/vault/api" + "sigs.k8s.io/controller-runtime/pkg/client" + + v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/connection/store" + "github.com/crossplane/crossplane-runtime/pkg/connection/store/vault/kv" + "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/resource" +) + +// Error strings. +const ( + errNoConfig = "no Vault config provided" + errNewClient = "cannot create new client" + errExtractCABundle = "cannot extract ca bundle" + errAppendCABundle = "cannot append ca bundle" + errExtractToken = "cannot extract token" + errNoTokenProvided = "token auth configured but no token provided" + + errGet = "cannot get secret" + errApply = "cannot apply secret" + errDelete = "cannot delete secret" +) + +// KVClient is a Vault AdditiveKVClient Secrets engine client that supports both v1 and v2. +type KVClient interface { + Get(path string, secret *kv.Secret) error + Apply(path string, secret *kv.Secret, ao ...kv.ApplyOption) error + Delete(path string) error +} + +// SecretStore is a Vault Secret Store. +type SecretStore struct { + client KVClient + + defaultParentPath string +} + +// NewSecretStore returns a new Vault SecretStore. +func NewSecretStore(ctx context.Context, kube client.Client, cfg v1.SecretStoreConfig) (*SecretStore, error) { // nolint: gocyclo + // NOTE(turkenh): Adding linter exception for gocyclo since this function + // went a little over the limit due to the switch statements not because of + // some complex logic. + if cfg.Vault == nil { + return nil, errors.New(errNoConfig) + } + vCfg := api.DefaultConfig() + vCfg.Address = cfg.Vault.Server + + if cfg.Vault.CABundle != nil { + ca, err := resource.CommonCredentialExtractor(ctx, cfg.Vault.CABundle.Source, kube, cfg.Vault.CABundle.CommonCredentialSelectors) + if err != nil { + return nil, errors.Wrap(err, errExtractCABundle) + } + pool := x509.NewCertPool() + if ok := pool.AppendCertsFromPEM(ca); !ok { + return nil, errors.Wrap(err, errAppendCABundle) + } + vCfg.HttpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = pool + } + + c, err := api.NewClient(vCfg) + if err != nil { + return nil, errors.Wrap(err, errNewClient) + } + + switch cfg.Vault.Auth.Method { + case v1.VaultAuthToken: + if cfg.Vault.Auth.Token == nil { + return nil, errors.New(errNoTokenProvided) + } + t, err := resource.CommonCredentialExtractor(ctx, cfg.Vault.Auth.Token.Source, kube, cfg.Vault.Auth.Token.CommonCredentialSelectors) + if err != nil { + return nil, errors.Wrap(err, errExtractToken) + } + c.SetToken(string(t)) + default: + return nil, errors.Errorf("%q is not supported as an auth method", cfg.Vault.Auth.Method) + } + + var kvClient KVClient + switch *cfg.Vault.Version { + case v1.VaultKVVersionV1: + kvClient = kv.NewV1Client(c.Logical(), cfg.Vault.MountPath) + case v1.VaultKVVersionV2: + kvClient = kv.NewV2Client(c.Logical(), cfg.Vault.MountPath) + } + + return &SecretStore{ + client: kvClient, + defaultParentPath: cfg.DefaultScope, + }, nil +} + +// ReadKeyValues reads and returns key value pairs for a given Vault Secret. +func (ss *SecretStore) ReadKeyValues(_ context.Context, n store.ScopedName, s *store.Secret) error { + kvs := &kv.Secret{} + if err := ss.client.Get(ss.path(n), kvs); resource.Ignore(kv.IsNotFound, err) != nil { + return errors.Wrap(err, errGet) + } + + s.ScopedName = n + s.Data = keyValuesFromData(kvs.Data) + if len(kvs.CustomMeta) > 0 { + s.Metadata = &v1.ConnectionSecretMetadata{ + Labels: kvs.CustomMeta, + } + } + return nil +} + +// WriteKeyValues writes key value pairs to a given Vault Secret. +func (ss *SecretStore) WriteKeyValues(_ context.Context, s *store.Secret, wo ...store.WriteOption) (changed bool, err error) { + ao := applyOptions(wo...) + ao = append(ao, kv.AllowUpdateIf(func(current, desired *kv.Secret) bool { + return !cmp.Equal(current, desired, cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(kv.Secret{})) + })) + + err = ss.client.Apply(ss.path(s.ScopedName), kv.NewSecret(dataFromKeyValues(s.Data), s.GetLabels()), ao...) + if resource.IsNotAllowed(err) { + // The update was not allowed because it was a no-op. + return false, nil + } + if err != nil { + return false, errors.Wrap(err, errApply) + } + return true, nil +} + +// DeleteKeyValues delete key value pairs from a given Vault Secret. +// If no kv specified, the whole secret instance is deleted. +// If kv specified, those would be deleted and secret instance will be deleted +// only if there is no Data left. +func (ss *SecretStore) DeleteKeyValues(_ context.Context, s *store.Secret, do ...store.DeleteOption) error { + Secret := &kv.Secret{} + err := ss.client.Get(ss.path(s.ScopedName), Secret) + if kv.IsNotFound(err) { + // Secret already deleted, nothing to do. + return nil + } + if err != nil { + return errors.Wrap(err, errGet) + } + + for _, o := range do { + if err = o(context.Background(), s); err != nil { + return err + } + } + + for k := range s.Data { + delete(Secret.Data, k) + } + if len(s.Data) == 0 || len(Secret.Data) == 0 { + // Secret is deleted only if: + // - No kv to delete specified as input + // - No data left in the secret + return errors.Wrap(ss.client.Delete(ss.path(s.ScopedName)), errDelete) + } + // If there are still keys left, update the secret with the remaining. + return errors.Wrap(ss.client.Apply(ss.path(s.ScopedName), Secret), errApply) +} + +func (ss *SecretStore) path(s store.ScopedName) string { + if s.Scope != "" { + return filepath.Join(s.Scope, s.Name) + } + return filepath.Join(ss.defaultParentPath, s.Name) +} + +func applyOptions(wo ...store.WriteOption) []kv.ApplyOption { + ao := make([]kv.ApplyOption, len(wo)) + for i := range wo { + o := wo[i] + ao[i] = func(current, desired *kv.Secret) error { + cs := &store.Secret{ + Metadata: &v1.ConnectionSecretMetadata{ + Labels: current.CustomMeta, + }, + Data: keyValuesFromData(current.Data), + } + ds := &store.Secret{ + Metadata: &v1.ConnectionSecretMetadata{ + Labels: desired.CustomMeta, + }, + Data: keyValuesFromData(desired.Data), + } + if err := o(context.Background(), cs, ds); err != nil { + return err + } + desired.CustomMeta = ds.GetLabels() + desired.Data = dataFromKeyValues(ds.Data) + return nil + } + } + return ao +} + +func keyValuesFromData(data map[string]string) store.KeyValues { + if len(data) == 0 { + return nil + } + kv := make(store.KeyValues, len(data)) + for k, v := range data { + kv[k] = []byte(v) + } + return kv +} + +func dataFromKeyValues(kv store.KeyValues) map[string]string { + if len(kv) == 0 { + return nil + } + data := make(map[string]string, len(kv)) + for k, v := range kv { + // NOTE(turkenh): vault stores values as strings. So we convert []byte + // to string before writing to Vault. + data[k] = string(v) + } + return data +} diff --git a/pkg/connection/store/vault/store_test.go b/pkg/connection/store/vault/store_test.go new file mode 100644 index 000000000..2e1d6d8f0 --- /dev/null +++ b/pkg/connection/store/vault/store_test.go @@ -0,0 +1,827 @@ +/* + 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 vault + +import ( + "context" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + + v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/connection/store" + "github.com/crossplane/crossplane-runtime/pkg/connection/store/vault/fake" + "github.com/crossplane/crossplane-runtime/pkg/connection/store/vault/kv" + "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/test" +) + +const ( + parentPathDefault = "crossplane-system" + + secretName = "conn-unittests" +) + +var ( + errBoom = errors.New("boom") +) + +func TestSecretStoreReadKeyValues(t *testing.T) { + type args struct { + client KVClient + defaultParentPath string + name store.ScopedName + } + type want struct { + out *store.Secret + err error + } + cases := map[string]struct { + reason string + args + want + }{ + "ErrorWhileGetting": { + reason: "Should return a proper error if secret cannot be obtained", + args: args{ + client: &fake.KVClient{ + GetFn: func(path string, secret *kv.Secret) error { + return errBoom + }, + }, + defaultParentPath: parentPathDefault, + name: store.ScopedName{ + Name: secretName, + }, + }, + want: want{ + out: &store.Secret{}, + err: errors.Wrap(errBoom, errGet), + }, + }, + "SuccessfulGetWithDefaultScope": { + reason: "Should return key values from a secret with default scope", + args: args{ + client: &fake.KVClient{ + GetFn: func(path string, secret *kv.Secret) error { + if diff := cmp.Diff(filepath.Join(parentPathDefault, secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + secret.Data = map[string]string{ + "key1": "val1", + "key2": "val2", + } + return nil + }, + }, + defaultParentPath: parentPathDefault, + name: store.ScopedName{ + Name: secretName, + }, + }, + want: want{ + out: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + }, + Data: store.KeyValues{ + "key1": []byte("val1"), + "key2": []byte("val2"), + }, + }, + err: nil, + }, + }, + "SuccessfulGetWithCustomScope": { + reason: "Should return key values from a secret with custom scope", + args: args{ + client: &fake.KVClient{ + GetFn: func(path string, secret *kv.Secret) error { + if diff := cmp.Diff(filepath.Join("another-scope", secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + secret.Data = map[string]string{ + "key1": "val1", + "key2": "val2", + } + return nil + }, + }, + defaultParentPath: parentPathDefault, + name: store.ScopedName{ + Name: secretName, + Scope: "another-scope", + }, + }, + want: want{ + out: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + Scope: "another-scope", + }, + Data: store.KeyValues{ + "key1": []byte("val1"), + "key2": []byte("val2"), + }, + }, + err: nil, + }, + }, + "SuccessfulGetWithMetadata": { + reason: "Should return both data and metadata.", + args: args{ + client: &fake.KVClient{ + GetFn: func(path string, secret *kv.Secret) error { + if diff := cmp.Diff(filepath.Join(parentPathDefault, secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + secret.Data = map[string]string{ + "key1": "val1", + "key2": "val2", + } + secret.CustomMeta = map[string]string{ + "foo": "bar", + } + return nil + }, + }, + defaultParentPath: parentPathDefault, + name: store.ScopedName{ + Name: secretName, + }, + }, + want: want{ + out: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + }, + Data: store.KeyValues{ + "key1": []byte("val1"), + "key2": []byte("val2"), + }, + Metadata: &v1.ConnectionSecretMetadata{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + err: nil, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + ss := &SecretStore{ + client: tc.args.client, + defaultParentPath: tc.args.defaultParentPath, + } + s := &store.Secret{} + err := ss.ReadKeyValues(context.Background(), tc.args.name, s) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nss.ReadKeyValues(...): -want error, +got error:\n%s", tc.reason, diff) + } + + if diff := cmp.Diff(tc.want.out, s); diff != "" { + t.Errorf("\n%s\nss.ReadKeyValues(...): -want, +got:\n%s", tc.reason, diff) + } + }) + } +} + +func TestSecretStoreWriteKeyValues(t *testing.T) { + type args struct { + client KVClient + defaultParentPath string + secret *store.Secret + + wo []store.WriteOption + } + type want struct { + changed bool + err error + } + cases := map[string]struct { + reason string + args + want + }{ + "ErrWhileApplying": { + reason: "Should successfully write key values", + args: args{ + client: &fake.KVClient{ + ApplyFn: func(path string, secret *kv.Secret, ao ...kv.ApplyOption) error { + return errBoom + }, + }, + defaultParentPath: parentPathDefault, + + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + }, + Data: store.KeyValues{ + "key1": []byte("val1"), + "key2": []byte("val2"), + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errApply), + }, + }, + "FailedWriteOption": { + reason: "Should return a proper error if supplied write option fails", + args: args{ + client: &fake.KVClient{ + ApplyFn: func(path string, secret *kv.Secret, ao ...kv.ApplyOption) error { + for _, o := range ao { + if err := o(&kv.Secret{}, secret); err != nil { + return err + } + } + return nil + }, + }, + defaultParentPath: parentPathDefault, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + }, + Data: store.KeyValues{ + "key1": []byte("val1"), + "key2": []byte("val2"), + }, + }, + wo: []store.WriteOption{ + func(ctx context.Context, current, desired *store.Secret) error { + return errBoom + }, + }, + }, + want: want{ + changed: false, + err: errors.Wrap(errBoom, errApply), + }, + }, + "SuccessfulWriteOption": { + reason: "Should return a no error if supplied write option succeeds", + args: args{ + client: &fake.KVClient{ + ApplyFn: func(path string, secret *kv.Secret, ao ...kv.ApplyOption) error { + for _, o := range ao { + if err := o(&kv.Secret{ + Data: map[string]string{ + "key1": "val1", + "key2": "val2", + }, + CustomMeta: map[string]string{ + "foo": "bar", + }, + }, secret); err != nil { + return err + } + } + return nil + }, + }, + defaultParentPath: parentPathDefault, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + }, + Data: store.KeyValues{ + "key1": []byte("val1"), + "key2": []byte("val2"), + }, + }, + wo: []store.WriteOption{ + func(ctx context.Context, current, desired *store.Secret) error { + desired.Data["customkey"] = []byte("customval") + desired.Metadata = &v1.ConnectionSecretMetadata{ + Labels: map[string]string{ + "foo": "baz", + }, + } + return nil + }, + }, + }, + want: want{ + changed: true, + }, + }, + "AlreadyUpToDate": { + reason: "Should return no error and changed as false if secret is already up to date", + args: args{ + client: &fake.KVClient{ + ApplyFn: func(path string, secret *kv.Secret, ao ...kv.ApplyOption) error { + for _, o := range ao { + if err := o(&kv.Secret{ + Data: map[string]string{ + "key1": "val1", + "key2": "val2", + }, + }, secret); err != nil { + return err + } + } + return nil + }, + }, + defaultParentPath: parentPathDefault, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + }, + Data: store.KeyValues{ + "key1": []byte("val1"), + "key2": []byte("val2"), + }, + }, + }, + want: want{ + changed: false, + err: nil, + }, + }, + "SuccessfulWrite": { + reason: "Should successfully write key values", + args: args{ + client: &fake.KVClient{ + ApplyFn: func(path string, secret *kv.Secret, ao ...kv.ApplyOption) error { + if diff := cmp.Diff(filepath.Join(parentPathDefault, secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + if diff := cmp.Diff(map[string]string{ + "key1": "val1", + "key2": "val2", + }, secret.Data); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return nil + }, + }, + defaultParentPath: parentPathDefault, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + }, + Data: store.KeyValues{ + "key1": []byte("val1"), + "key2": []byte("val2"), + }, + }, + }, + want: want{ + changed: true, + }, + }, + "SuccessfulWriteWithMetadata": { + reason: "Should successfully write key values", + args: args{ + client: &fake.KVClient{ + ApplyFn: func(path string, secret *kv.Secret, ao ...kv.ApplyOption) error { + if diff := cmp.Diff(filepath.Join(parentPathDefault, secretName), path); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + if diff := cmp.Diff(map[string]string{ + "key1": "val1", + "key2": "val2", + }, secret.Data); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + if diff := cmp.Diff(map[string]string{ + "foo": "bar", + }, secret.CustomMeta); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return nil + }, + }, + defaultParentPath: parentPathDefault, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + }, + Metadata: &v1.ConnectionSecretMetadata{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + Data: store.KeyValues{ + "key1": []byte("val1"), + "key2": []byte("val2"), + }, + }, + }, + want: want{ + changed: true, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + ss := &SecretStore{ + client: tc.args.client, + defaultParentPath: tc.args.defaultParentPath, + } + changed, err := ss.WriteKeyValues(context.Background(), tc.args.secret, tc.args.wo...) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nss.WriteKeyValues(...): -want error, +got error:\n%s", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.changed, changed); diff != "" { + t.Errorf("\n%s\nss.WriteKeyValues(...): -want changed, +got changed:\n%s", tc.reason, diff) + } + }) + } +} + +func TestSecretStoreDeleteKeyValues(t *testing.T) { + type args struct { + client KVClient + defaultParentPath string + secret *store.Secret + + do []store.DeleteOption + } + type want struct { + err error + } + cases := map[string]struct { + reason string + args + want + }{ + "ErrorGettingSecret": { + reason: "Should return a proper error if getting secret fails.", + args: args{ + client: &fake.KVClient{ + GetFn: func(path string, secret *kv.Secret) error { + return errBoom + }, + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errGet), + }, + }, + "AlreadyDeleted": { + reason: "Should return no error if connection secret already deleted.", + args: args{ + client: &fake.KVClient{ + GetFn: func(path string, secret *kv.Secret) error { + return errors.New(kv.ErrNotFound) + }, + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + }, + }, + }, + want: want{ + err: nil, + }, + }, + "DeletesSecretIfNoKVProvided": { + reason: "Should delete whole secret if no kv provided as input", + args: args{ + client: &fake.KVClient{ + GetFn: func(path string, secret *kv.Secret) error { + secret.Data = map[string]string{ + "key1": "val1", + "key2": "val2", + "key3": "val3", + } + return nil + }, + DeleteFn: func(path string) error { + return nil + }, + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + }, + }, + }, + want: want{ + err: nil, + }, + }, + "ErrorUpdatingSecretWithRemaining": { + reason: "Should return a proper error if updating secret with remaining keys fails.", + args: args{ + client: &fake.KVClient{ + GetFn: func(path string, secret *kv.Secret) error { + secret.Data = map[string]string{ + "key1": "val1", + "key2": "val2", + "key3": "val3", + } + return nil + }, + ApplyFn: func(path string, secret *kv.Secret, ao ...kv.ApplyOption) error { + return errBoom + }, + DeleteFn: func(path string) error { + return errors.New("unexpected delete call") + }, + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + }, + Data: map[string][]byte{ + "key1": []byte("val1"), + "key2": []byte("val2"), + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errApply), + }, + }, + "UpdatesSecretByRemovingProvidedKeys": { + reason: "Should only delete provided keys and should not delete secret if kv provided as input.", + args: args{ + client: &fake.KVClient{ + GetFn: func(path string, secret *kv.Secret) error { + secret.Data = map[string]string{ + "key1": "val1", + "key2": "val2", + "key3": "val3", + } + return nil + }, + ApplyFn: func(path string, secret *kv.Secret, ao ...kv.ApplyOption) error { + if diff := cmp.Diff(map[string]string{ + "key3": "val3", + }, secret.Data); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } + return nil + }, + DeleteFn: func(path string) error { + return errors.New("unexpected delete call") + }, + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + }, + Data: map[string][]byte{ + "key1": []byte("val1"), + "key2": []byte("val2"), + }, + }, + }, + want: want{ + err: nil, + }, + }, + "ErrorDeletingSecret": { + reason: "Should return a proper error if deleting the secret after no keys left fails.", + args: args{ + client: &fake.KVClient{ + GetFn: func(path string, secret *kv.Secret) error { + secret.Data = map[string]string{ + "key1": "val1", + "key2": "val2", + "key3": "val3", + } + return nil + }, + DeleteFn: func(path string) error { + return errBoom + }, + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + }, + Data: map[string][]byte{ + "key1": []byte("val1"), + "key2": []byte("val2"), + "key3": []byte("val3"), + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errDelete), + }, + }, + "FailedDeleteOption": { + reason: "Should return a proper error if provided delete option fails.", + args: args{ + client: &fake.KVClient{ + GetFn: func(path string, secret *kv.Secret) error { + secret.Data = map[string]string{ + "key1": "val1", + } + return nil + }, + DeleteFn: func(path string) error { + return nil + }, + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + }, + }, + do: []store.DeleteOption{ + func(ctx context.Context, secret *store.Secret) error { + return errBoom + }, + }, + }, + want: want{ + err: errBoom, + }, + }, + "SuccessfulDeleteNoKeysLeft": { + reason: "Should delete the secret if no keys left.", + args: args{ + client: &fake.KVClient{ + GetFn: func(path string, secret *kv.Secret) error { + secret.Data = map[string]string{ + "key1": "val1", + "key2": "val2", + "key3": "val3", + } + return nil + }, + DeleteFn: func(path string) error { + return nil + }, + }, + secret: &store.Secret{ + ScopedName: store.ScopedName{ + Name: secretName, + }, + Data: map[string][]byte{ + "key1": []byte("val1"), + "key2": []byte("val2"), + "key3": []byte("val3"), + }, + }, + do: []store.DeleteOption{ + func(ctx context.Context, secret *store.Secret) error { + return nil + }, + }, + }, + want: want{ + err: nil, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + ss := &SecretStore{ + client: tc.args.client, + defaultParentPath: tc.args.defaultParentPath, + } + err := ss.DeleteKeyValues(context.Background(), tc.args.secret, tc.args.do...) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nss.ReadKeyValues(...): -want error, +got error:\n%s", tc.reason, diff) + } + }) + } +} + +func TestNewSecretStore(t *testing.T) { + kvv2 := v1.VaultKVVersionV2 + type args struct { + kube client.Client + cfg v1.SecretStoreConfig + } + type want struct { + err error + } + cases := map[string]struct { + reason string + args + want + }{ + "InvalidAuthConfig": { + reason: "Should return a proper error if vault auth configuration is not valid.", + args: args{ + cfg: v1.SecretStoreConfig{ + Vault: &v1.VaultSecretStoreConfig{ + Auth: v1.VaultAuthConfig{ + Method: v1.VaultAuthToken, + Token: nil, + }, + }, + }, + }, + want: want{ + err: errors.New(errNoTokenProvided), + }, + }, + "NoTokenSecret": { + reason: "Should return a proper error if configured vault token secret does not exist.", + args: args{ + kube: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + return kerrors.NewNotFound(schema.GroupResource{}, "vault-token") + }), + }, + cfg: v1.SecretStoreConfig{ + Vault: &v1.VaultSecretStoreConfig{ + Auth: v1.VaultAuthConfig{ + Method: v1.VaultAuthToken, + Token: &v1.VaultAuthTokenConfig{ + Source: v1.CredentialsSourceSecret, + CommonCredentialSelectors: v1.CommonCredentialSelectors{ + SecretRef: &v1.SecretKeySelector{ + SecretReference: v1.SecretReference{ + Name: "vault-token", + Namespace: "crossplane-system", + }, + Key: "token", + }, + }, + }, + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errors.Wrap(kerrors.NewNotFound(schema.GroupResource{}, "vault-token"), "cannot get credentials secret"), errExtractToken), + }, + }, + "SuccessfulStore": { + reason: "Should return no error after building store successfully.", + args: args{ + kube: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + *obj.(*corev1.Secret) = corev1.Secret{ + Data: map[string][]byte{ + "token": []byte("t0ps3cr3t"), + }, + } + return nil + }), + }, + cfg: v1.SecretStoreConfig{ + Vault: &v1.VaultSecretStoreConfig{ + Version: &kvv2, + Auth: v1.VaultAuthConfig{ + Method: v1.VaultAuthToken, + Token: &v1.VaultAuthTokenConfig{ + Source: v1.CredentialsSourceSecret, + CommonCredentialSelectors: v1.CommonCredentialSelectors{ + SecretRef: &v1.SecretKeySelector{ + SecretReference: v1.SecretReference{ + Name: "vault-token", + Namespace: "crossplane-system", + }, + Key: "token", + }, + }, + }, + }, + }, + }, + }, + want: want{ + err: nil, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + _, err := NewSecretStore(context.Background(), tc.args.kube, tc.args.cfg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nNewSecretStore(...): -want error, +got error:\n%s", tc.reason, diff) + } + }) + } +} diff --git a/pkg/connection/stores.go b/pkg/connection/stores.go new file mode 100644 index 000000000..cdc7f1601 --- /dev/null +++ b/pkg/connection/stores.go @@ -0,0 +1,46 @@ +/* + 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 connection + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" + + v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/connection/store/kubernetes" + "github.com/crossplane/crossplane-runtime/pkg/connection/store/vault" + "github.com/crossplane/crossplane-runtime/pkg/errors" +) + +const ( + errFmtUnknownSecretStore = "unknown secret store type: %q" +) + +// RuntimeStoreBuilder builds and returns a Store for any supported Store type +// in a given config. +// +// All in-tree connection Store implementations needs to be registered here. +func RuntimeStoreBuilder(ctx context.Context, local client.Client, cfg v1.SecretStoreConfig) (Store, error) { + switch *cfg.Type { + case v1.SecretStoreKubernetes: + return kubernetes.NewSecretStore(ctx, local, cfg) + case v1.SecretStoreVault: + return vault.NewSecretStore(ctx, local, cfg) + } + return nil, errors.Errorf(errFmtUnknownSecretStore, *cfg.Type) +} diff --git a/pkg/controller/engine.go b/pkg/controller/engine.go index 4f23b4a6e..9967dd421 100644 --- a/pkg/controller/engine.go +++ b/pkg/controller/engine.go @@ -21,7 +21,6 @@ import ( "context" "sync" - "github.com/pkg/errors" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" @@ -30,6 +29,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/crossplane/crossplane-runtime/pkg/errors" ) // Error strings diff --git a/pkg/controller/engine_test.go b/pkg/controller/engine_test.go index cc060101a..1abca9f36 100644 --- a/pkg/controller/engine_test.go +++ b/pkg/controller/engine_test.go @@ -22,7 +22,6 @@ import ( "time" "github.com/google/go-cmp/cmp" - "github.com/pkg/errors" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -31,6 +30,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/source" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/pkg/test" ) diff --git a/pkg/controller/options.go b/pkg/controller/options.go new file mode 100644 index 000000000..a20d4c5ff --- /dev/null +++ b/pkg/controller/options.go @@ -0,0 +1,68 @@ +/* +Copyright 2021 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 controller + +import ( + "time" + + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/controller" + + "github.com/crossplane/crossplane-runtime/pkg/feature" + "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/pkg/ratelimiter" +) + +// DefaultOptions returns a functional set of options with conservative +// defaults. +func DefaultOptions() Options { + return Options{ + Logger: logging.NewNopLogger(), + GlobalRateLimiter: ratelimiter.NewGlobal(1), + PollInterval: 1 * time.Minute, + MaxConcurrentReconciles: 1, + Features: &feature.Flags{}, + } +} + +// Options frequently used by most Crossplane controllers. +type Options struct { + // The Logger controllers should use. + Logger logging.Logger + + // The GlobalRateLimiter used by this controller manager. The rate of + // reconciles across all controllers will be subject to this limit. + GlobalRateLimiter workqueue.RateLimiter + + // PollInterval at which each controller should speculatively poll to + // determine whether it has work to do. + PollInterval time.Duration + + // MaxConcurrentReconciles for each controller. + MaxConcurrentReconciles int + + // Features that should be enabled. + Features *feature.Flags +} + +// ForControllerRuntime extracts options for controller-runtime. +func (o Options) ForControllerRuntime() controller.Options { + return controller.Options{ + MaxConcurrentReconciles: o.MaxConcurrentReconciles, + RateLimiter: ratelimiter.NewController(), + } +} diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go new file mode 100644 index 000000000..24cc306f5 --- /dev/null +++ b/pkg/errors/errors.go @@ -0,0 +1,128 @@ +/* +Copyright 2021 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 errors is a github.com/pkg/errors compatible API for native errors. +// It includes only the subset of the github.com/pkg/errors API that is used by +// the Crossplane project. +package errors + +import ( + "errors" + "fmt" +) + +// New returns an error that formats as the given text. Each call to New returns +// a distinct error value even if the text is identical. +func New(text string) error { return errors.New(text) } + +// Is reports whether any error in err's chain matches target. +// +// The chain consists of err itself followed by the sequence of errors obtained +// by repeatedly calling Unwrap. +// +// An error is considered to match a target if it is equal to that target or if +// it implements a method Is(error) bool such that Is(target) returns true. +// +// An error type might provide an Is method so it can be treated as equivalent +// to an existing error. For example, if MyError defines +// +// func (m MyError) Is(target error) bool { return target == fs.ErrExist } +// +// then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for +// an example in the standard library. +func Is(err, target error) bool { return errors.Is(err, target) } + +// As finds the first error in err's chain that matches target, and if so, sets +// target to that error value and returns true. Otherwise, it returns false. +// +// The chain consists of err itself followed by the sequence of errors obtained +// by repeatedly calling Unwrap. +// +// An error matches target if the error's concrete value is assignable to the +// value pointed to by target, or if the error has a method As(interface{}) bool +// such that As(target) returns true. In the latter case, the As method is +// responsible for setting target. +// +// An error type might provide an As method so it can be treated as if it were a +// different error type. +// +// As panics if target is not a non-nil pointer to either a type that implements +// error, or to any interface type. +func As(err error, target interface{}) bool { return errors.As(err, target) } + +// Unwrap returns the result of calling the Unwrap method on err, if err's type +// contains an Unwrap method returning error. Otherwise, Unwrap returns nil. +func Unwrap(err error) error { return errors.Unwrap(err) } + +// Errorf formats according to a format specifier and returns the string as a +// value that satisfies error. +// +// If the format specifier includes a %w verb with an error operand, the +// returned error will implement an Unwrap method returning the operand. It is +// invalid to include more than one %w verb or to supply it with an operand that +// does not implement the error interface. The %w verb is otherwise a synonym +// for %v. +func Errorf(format string, a ...interface{}) error { return fmt.Errorf(format, a...) } + +// WithMessage annotates err with a new message. If err is nil, WithMessage +// returns nil. +func WithMessage(err error, message string) error { + if err == nil { + return nil + } + return fmt.Errorf("%s: %w", message, err) +} + +// WithMessagef annotates err with the format specifier. If err is nil, +// WithMessagef returns nil. +func WithMessagef(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err) +} + +// Wrap is an alias for WithMessage. +func Wrap(err error, message string) error { + return WithMessage(err, message) +} + +// Wrapf is an alias for WithMessagef +func Wrapf(err error, format string, args ...interface{}) error { + return WithMessagef(err, format, args...) +} + +// Cause calls Unwrap on each error it finds. It returns the first error it +// finds that does not have an Unwrap method - i.e. the first error that was not +// the result of a Wrap call, a Wrapf call, or an Errorf call with %w wrapping. +func Cause(err error) error { + type wrapped interface { + Unwrap() error + } + + for err != nil { + // We're ignoring errorlint telling us to use errors.As because + // we actually do want to check the outermost error. + //nolint:errorlint + w, ok := err.(wrapped) + if !ok { + return err + } + err = w.Unwrap() + } + + return err +} diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go new file mode 100644 index 000000000..be518950f --- /dev/null +++ b/pkg/errors/errors_test.go @@ -0,0 +1,126 @@ +/* +Copyright 2021 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 errors + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/crossplane/crossplane-runtime/pkg/test" +) + +func TestWrap(t *testing.T) { + type args struct { + err error + message string + } + cases := map[string]struct { + args args + want error + }{ + "NilError": { + args: args{ + err: nil, + message: "very useful context", + }, + want: nil, + }, + "NonNilError": { + args: args{ + err: New("boom"), + message: "very useful context", + }, + want: Errorf("very useful context: %w", New("boom")), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := Wrap(tc.args.err, tc.args.message) + if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { + t.Errorf("Wrap(...): -want, +got:\n%s", diff) + } + }) + } +} + +func TestWrapf(t *testing.T) { + type args struct { + err error + message string + args []interface{} + } + cases := map[string]struct { + args args + want error + }{ + "NilError": { + args: args{ + err: nil, + message: "very useful context", + }, + want: nil, + }, + "NonNilError": { + args: args{ + err: New("boom"), + message: "very useful context about %s", + args: []interface{}{"ducks"}, + }, + want: Errorf("very useful context about %s: %w", "ducks", New("boom")), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := Wrapf(tc.args.err, tc.args.message, tc.args.args...) + if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { + t.Errorf("Wrapf(...): -want, +got:\n%s", diff) + } + }) + } +} + +func TestCause(t *testing.T) { + cases := map[string]struct { + err error + want error + }{ + "NilError": { + err: nil, + want: nil, + }, + "BareError": { + err: New("boom"), + want: New("boom"), + }, + "WrappedError": { + err: Wrap(Wrap(New("boom"), "interstitial context"), "very important context"), + want: New("boom"), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := Cause(tc.err) + if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { + t.Errorf("Cause(...): -want, +got:\n%s", diff) + } + }) + } +} diff --git a/pkg/feature/feature.go b/pkg/feature/feature.go new file mode 100644 index 000000000..e88a5a07b --- /dev/null +++ b/pkg/feature/feature.go @@ -0,0 +1,51 @@ +/* +Copyright 2021 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 feature contains utilities for managing Crossplane features. +package feature + +import ( + "sync" +) + +// A Flag enables a particular feature. +type Flag string + +// Flags that are enabled. The zero value - i.e. &feature.Flags{} - is usable. +type Flags struct { + m sync.RWMutex + enabled map[Flag]bool +} + +// Enable a feature flag. +func (fs *Flags) Enable(f Flag) { + fs.m.Lock() + if fs.enabled == nil { + fs.enabled = make(map[Flag]bool) + } + fs.enabled[f] = true + fs.m.Unlock() +} + +// Enabled returns true if the supplied feature flag is enabled. +func (fs *Flags) Enabled(f Flag) bool { + if fs == nil { + return false + } + fs.m.RLock() + defer fs.m.RUnlock() + return fs.enabled[f] +} diff --git a/pkg/feature/feature_test.go b/pkg/feature/feature_test.go new file mode 100644 index 000000000..18ddf2e3e --- /dev/null +++ b/pkg/feature/feature_test.go @@ -0,0 +1,61 @@ +/* +Copyright 2021 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 feature + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestEnable(t *testing.T) { + var cool Flag = "cool" + + t.Run("EnableMutatesZeroValue", func(t *testing.T) { + f := &Flags{} + f.Enable(cool) + + want := true + got := f.Enabled(cool) + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("f.Enabled(...): -want, +got:\n%s", diff) + } + }) + + t.Run("EnabledOnEmptyFlagsReturnsFalse", func(t *testing.T) { + f := &Flags{} + + want := false + got := f.Enabled(cool) + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("f.Enabled(...): -want, +got:\n%s", diff) + } + }) + + t.Run("EnabledOnNilReturnsFalse", func(t *testing.T) { + var f *Flags + + want := false + got := f.Enabled(cool) + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("f.Enabled(...): -want, +got:\n%s", diff) + } + }) +} diff --git a/pkg/fieldpath/fieldpath.go b/pkg/fieldpath/fieldpath.go index c9a592a43..ade25cddf 100644 --- a/pkg/fieldpath/fieldpath.go +++ b/pkg/fieldpath/fieldpath.go @@ -48,7 +48,7 @@ import ( "strings" "unicode/utf8" - "github.com/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/errors" ) // A SegmentType within a field path; either a field within an object, or an @@ -78,7 +78,7 @@ func (sg Segments) String() string { for _, s := range sg { switch s.Type { case SegmentField: - if strings.ContainsRune(s.Field, period) { + if s.Field == wildcard || strings.ContainsRune(s.Field, period) { b.WriteString(fmt.Sprintf("[%s]", s.Field)) continue } @@ -138,6 +138,8 @@ const ( period = '.' leftBracket = '[' rightBracket = ']' + + wildcard = "*" ) type itemType int diff --git a/pkg/fieldpath/fieldpath_test.go b/pkg/fieldpath/fieldpath_test.go index ed77b22bb..3b76f139e 100644 --- a/pkg/fieldpath/fieldpath_test.go +++ b/pkg/fieldpath/fieldpath_test.go @@ -22,8 +22,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/test" ) @@ -56,6 +56,15 @@ func TestSegments(t *testing.T) { }, want: "data[.config.yml]", }, + "Wildcard": { + s: Segments{ + Field("spec"), + Field("containers"), + FieldOrIndex("*"), + Field("name"), + }, + want: "spec.containers[*].name", + }, } for name, tc := range cases { diff --git a/pkg/fieldpath/merge.go b/pkg/fieldpath/merge.go index e8c9517c5..8e33ec678 100644 --- a/pkg/fieldpath/merge.go +++ b/pkg/fieldpath/merge.go @@ -20,9 +20,9 @@ import ( "reflect" "github.com/imdario/mergo" - "github.com/pkg/errors" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" ) const ( diff --git a/pkg/fieldpath/paved.go b/pkg/fieldpath/paved.go index 184e41dbe..57508ab38 100644 --- a/pkg/fieldpath/paved.go +++ b/pkg/fieldpath/paved.go @@ -17,9 +17,12 @@ limitations under the License. package fieldpath import ( - "github.com/pkg/errors" + "strconv" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/json" + + "github.com/crossplane/crossplane-runtime/pkg/errors" ) type errNotFound struct { @@ -82,7 +85,10 @@ func (p *Paved) SetUnstructuredContent(content map[string]interface{}) { } func (p *Paved) getValue(s Segments) (interface{}, error) { - var it interface{} = p.object + return getValueFromInterface(p.object, s) +} + +func getValueFromInterface(it interface{}, s Segments) (interface{}, error) { for i, current := range s { final := i == len(s)-1 switch current.Type { @@ -98,7 +104,6 @@ func (p *Paved) getValue(s Segments) (interface{}, error) { return array[current.Index], nil } it = array[current.Index] - case SegmentField: object, ok := it.(map[string]interface{}) if !ok { @@ -119,6 +124,80 @@ func (p *Paved) getValue(s Segments) (interface{}, error) { return nil, nil } +// ExpandWildcards expands wildcards for a given field path. It returns an +// array of field paths with expanded values. Please note that expanded paths +// depend on the input data which is paved.object. +// +// Example: +// +// For a Paved object with the following data: []byte(`{"spec":{"containers":[{"name":"cool", "image": "latest", "args": ["start", "now", "debug"]}]}}`), +// ExpandWildcards("spec.containers[*].args[*]") returns: +// []string{"spec.containers[0].args[0]", "spec.containers[0].args[1]", "spec.containers[0].args[2]"}, +func (p *Paved) ExpandWildcards(path string) ([]string, error) { + segments, err := Parse(path) + if err != nil { + return nil, errors.Wrapf(err, "cannot parse path %q", path) + } + segmentsArray, err := expandWildcards(p.object, segments) + if err != nil { + return nil, errors.Wrapf(err, "cannot expand wildcards for segments: %q", segments) + } + paths := make([]string, len(segmentsArray)) + for i, s := range segmentsArray { + paths[i] = s.String() + } + return paths, nil +} + +// Note(turkenh): Explanation for nolint:gocyclo +// Even complexity turns out to be high, it is mostly because we have duplicate +// logic for arrays and maps and a couple of error handling. +func expandWildcards(data interface{}, segments Segments) ([]Segments, error) { //nolint:gocyclo + var res []Segments + it := data + for i, current := range segments { + // wildcards are regular fields with "*" as string + if current.Type == SegmentField && current.Field == wildcard { + switch mapOrArray := it.(type) { + case []interface{}: + for ix := range mapOrArray { + expanded := make(Segments, len(segments)) + copy(expanded, segments) + expanded = append(append(expanded[:i], FieldOrIndex(strconv.Itoa(ix))), expanded[i+1:]...) + r, err := expandWildcards(data, expanded) + if err != nil { + return nil, errors.Wrapf(err, "%q: cannot expand wildcards", expanded) + } + res = append(res, r...) + } + case map[string]interface{}: + for k := range mapOrArray { + expanded := make(Segments, len(segments)) + copy(expanded, segments) + expanded = append(append(expanded[:i], Field(k)), expanded[i+1:]...) + r, err := expandWildcards(data, expanded) + if err != nil { + return nil, errors.Wrapf(err, "%q: cannot expand wildcards", expanded) + } + res = append(res, r...) + } + default: + return nil, errors.Errorf("%q: unexpected wildcard usage", segments[:i]) + } + return res, nil + } + var err error + it, err = getValueFromInterface(data, segments[:i+1]) + if IsNotFound(err) { + return nil, nil + } + if err != nil { + return nil, err + } + } + return append(res, segments), nil +} + // GetValue of the supplied field path. func (p *Paved) GetValue(path string) (interface{}, error) { segments, err := Parse(path) diff --git a/pkg/fieldpath/paved_test.go b/pkg/fieldpath/paved_test.go index fbe998030..b8c4ee365 100644 --- a/pkg/fieldpath/paved_test.go +++ b/pkg/fieldpath/paved_test.go @@ -20,11 +20,12 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/pkg/errors" + "github.com/google/go-cmp/cmp/cmpopts" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/json" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/test" ) @@ -828,3 +829,162 @@ func TestSetValue(t *testing.T) { }) } } + +func TestExpandWildcards(t *testing.T) { + type want struct { + expanded []string + err error + } + cases := map[string]struct { + reason string + path string + data []byte + want want + }{ + "NoWildcardExisting": { + reason: "It should return same path if no wildcard in an existing path", + path: "password", + data: []byte(`{"password":"top-secret"}`), + want: want{ + expanded: []string{"password"}, + }, + }, + "NoWildcardNonExisting": { + reason: "It should return no results if no wildcard in a non-existing path", + path: "username", + data: []byte(`{"password":"top-secret"}`), + want: want{ + expanded: []string{}, + }, + }, + "NestedNoWildcardExisting": { + reason: "It should return same path if no wildcard in an existing path", + path: "items[0][1]", + data: []byte(`{"items":[["a", "b"]]}`), + want: want{ + expanded: []string{"items[0][1]"}, + }, + }, + "NestedNoWildcardNonExisting": { + reason: "It should return no results if no wildcard in a non-existing path", + path: "items[0][5]", + data: []byte(`{"items":[["a", "b"]]}`), + want: want{ + expanded: []string{}, + }, + }, + "NestedArray": { + reason: "It should return all possible paths for an array", + path: "items[*][*]", + data: []byte(`{"items":[["a", "b", "c"], ["d"]]}`), + want: want{ + expanded: []string{"items[0][0]", "items[0][1]", "items[0][2]", "items[1][0]"}, + }, + }, + "KeysOfMap": { + reason: "It should return all possible paths for a map in proper syntax", + path: "items[*]", + data: []byte(`{"items":{ "key1": "val1", "key2.as.annotation": "val2"}}`), + want: want{ + expanded: []string{"items.key1", "items[key2.as.annotation]"}, + }, + }, + "ArrayOfObjects": { + reason: "It should return all possible paths for an array of objects", + path: "spec.containers[*][*]", + data: []byte(`{"spec":{"containers":[{"name":"cool", "image": "latest", "args": ["start", "now"]}]}}`), + want: want{ + expanded: []string{"spec.containers[0].name", "spec.containers[0].image", "spec.containers[0].args"}, + }, + }, + "MultiLayer": { + reason: "It should return all possible paths for a multilayer input", + path: "spec.containers[*].args[*]", + data: []byte(`{"spec":{"containers":[{"name":"cool", "image": "latest", "args": ["start", "now", "debug"]}]}}`), + want: want{ + expanded: []string{"spec.containers[0].args[0]", "spec.containers[0].args[1]", "spec.containers[0].args[2]"}, + }, + }, + "WildcardInTheBeginning": { + reason: "It should return all possible paths for a multilayer input with wildcard in the beginning", + path: "spec.containers[*].args[1]", + data: []byte(`{"spec":{"containers":[{"name":"cool", "image": "latest", "args": ["start", "now", "debug"]}]}}`), + want: want{ + expanded: []string{"spec.containers[0].args[1]"}, + }, + }, + "WildcardAtTheEnd": { + reason: "It should return all possible paths for a multilayer input with wildcard at the end", + path: "spec.containers[0].args[*]", + data: []byte(`{"spec":{"containers":[{"name":"cool", "image": "latest", "args": ["start", "now", "debug"]}]}}`), + want: want{ + expanded: []string{"spec.containers[0].args[0]", "spec.containers[0].args[1]", "spec.containers[0].args[2]"}, + }, + }, + "NoData": { + reason: "If there is no input data, no expanded fields could be found", + path: "metadata[*]", + data: nil, + want: want{ + expanded: []string{}, + }, + }, + "InsufficientContainers": { + reason: "Requesting a non-existent array element should return nothing", + path: "spec.containers[1].args[*]", + data: []byte(`{"spec":{"containers":[{"name":"cool"}]}}`), + want: want{ + expanded: []string{}, + }, + }, + "UnexpectedWildcard": { + reason: "Requesting a wildcard for an object should fail", + path: "spec.containers[0].name[*]", + data: []byte(`{"spec":{"containers":[{"name":"cool"}]}}`), + want: want{ + err: errors.Wrapf(errors.Errorf("%q: unexpected wildcard usage", "spec.containers[0].name"), "cannot expand wildcards for segments: %q", "spec.containers[0].name[*]"), + }, + }, + "NotAnArray": { + reason: "Indexing an object should fail", + path: "metadata[1]", + data: []byte(`{"metadata":{"nope":"cool"}}`), + want: want{ + err: errors.Wrapf(errors.New("metadata: not an array"), "cannot expand wildcards for segments: %q", "metadata[1]"), + }, + }, + "NotAnObject": { + reason: "Requesting a field in an array should fail", + path: "spec.containers[nope].name", + data: []byte(`{"spec":{"containers":[{"name":"cool"}]}}`), + want: want{ + err: errors.Wrapf(errors.New("spec.containers: not an object"), "cannot expand wildcards for segments: %q", "spec.containers.nope.name"), + }, + }, + "MalformedPath": { + reason: "Requesting an invalid field path should fail", + path: "spec[]", + want: want{ + err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + in := make(map[string]interface{}) + _ = json.Unmarshal(tc.data, &in) + p := Pave(in) + + got, err := p.ExpandWildcards(tc.path) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Fatalf("\np.ExpandWildcards(%s): %s: -want error, +got error:\n%s", tc.path, tc.reason, diff) + } + if diff := cmp.Diff(tc.want.expanded, got, cmpopts.SortSlices(func(x, y string) bool { + return x < y + })); diff != "" { + t.Errorf("\np.ExpandWildcards(%s): %s: -want, +got:\n%s", tc.path, tc.reason, diff) + } + }) + } +} diff --git a/pkg/meta/meta.go b/pkg/meta/meta.go index 71493990d..d6e56f47d 100644 --- a/pkg/meta/meta.go +++ b/pkg/meta/meta.go @@ -21,19 +21,43 @@ import ( "fmt" "hash/fnv" "strings" + "time" - "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" ) -// AnnotationKeyExternalName is the key in the annotations map of a resource for -// the name of the resource as it appears on provider's systems. -const AnnotationKeyExternalName = "crossplane.io/external-name" +const ( + // AnnotationKeyExternalName is the key in the annotations map of a + // resource for the name of the resource as it appears on provider's + // systems. + AnnotationKeyExternalName = "crossplane.io/external-name" + + // AnnotationKeyExternalCreatePending is the key in the annotations map + // of a resource that indicates the last time creation of the external + // resource was pending (i.e. about to happen). Its value must be an + // RFC3999 timestamp. + AnnotationKeyExternalCreatePending = "crossplane.io/external-create-pending" + + // AnnotationKeyExternalCreateSucceeded is the key in the annotations + // map of a resource that represents the last time the external resource + // was created successfully. Its value must be an RFC3339 timestamp, + // which can be used to determine how long ago a resource was created. + // This is useful for eventually consistent APIs that may take some time + // before the API called by Observe will report that a recently created + // external resource exists. + AnnotationKeyExternalCreateSucceeded = "crossplane.io/external-create-succeeded" + + // AnnotationKeyExternalCreateFailed is the key in the annotations map + // of a resource that indicates the last time creation of the external + // resource failed. Its value must be an RFC3999 timestamp. + AnnotationKeyExternalCreateFailed = "crossplane.io/external-create-failed" +) // Supported resources with all of these annotations will be fully or partially // propagated to the named resource of the same kind, assuming it exists and @@ -245,6 +269,90 @@ func SetExternalName(o metav1.Object, name string) { AddAnnotations(o, map[string]string{AnnotationKeyExternalName: name}) } +// GetExternalCreatePending returns the time at which the external resource +// was most recently pending creation. +func GetExternalCreatePending(o metav1.Object) time.Time { + a := o.GetAnnotations()[AnnotationKeyExternalCreatePending] + t, err := time.Parse(time.RFC3339, a) + if err != nil { + return time.Time{} + } + return t +} + +// SetExternalCreatePending sets the time at which the external resource was +// most recently pending creation to the supplied time. +func SetExternalCreatePending(o metav1.Object, t time.Time) { + AddAnnotations(o, map[string]string{AnnotationKeyExternalCreatePending: t.Format(time.RFC3339)}) +} + +// GetExternalCreateSucceeded returns the time at which the external resource +// was most recently created. +func GetExternalCreateSucceeded(o metav1.Object) time.Time { + a := o.GetAnnotations()[AnnotationKeyExternalCreateSucceeded] + t, err := time.Parse(time.RFC3339, a) + if err != nil { + return time.Time{} + } + return t +} + +// SetExternalCreateSucceeded sets the time at which the external resource was +// most recently created to the supplied time. +func SetExternalCreateSucceeded(o metav1.Object, t time.Time) { + AddAnnotations(o, map[string]string{AnnotationKeyExternalCreateSucceeded: t.Format(time.RFC3339)}) +} + +// GetExternalCreateFailed returns the time at which the external resource +// recently failed to create. +func GetExternalCreateFailed(o metav1.Object) time.Time { + a := o.GetAnnotations()[AnnotationKeyExternalCreateFailed] + t, err := time.Parse(time.RFC3339, a) + if err != nil { + return time.Time{} + } + return t +} + +// SetExternalCreateFailed sets the time at which the external resource most +// recently failed to create. +func SetExternalCreateFailed(o metav1.Object, t time.Time) { + AddAnnotations(o, map[string]string{AnnotationKeyExternalCreateFailed: t.Format(time.RFC3339)}) +} + +// ExternalCreateIncomplete returns true if creation of the external resource +// appears to be incomplete. We deem creation to be incomplete if the 'external +// create pending' annotation is the newest of all tracking annotations that are +// set (i.e. pending, succeeded, and failed). +func ExternalCreateIncomplete(o metav1.Object) bool { + pending := GetExternalCreatePending(o) + succeeded := GetExternalCreateSucceeded(o) + failed := GetExternalCreateFailed(o) + + // If creation never started it can't be incomplete. + if pending.IsZero() { + return false + } + + latest := succeeded + if failed.After(succeeded) { + latest = failed + } + + return pending.After(latest) +} + +// ExternalCreateSucceededDuring returns true if creation of the external +// resource that corresponds to the supplied managed resource succeeded within +// the supplied duration. +func ExternalCreateSucceededDuring(o metav1.Object, d time.Duration) bool { + t := GetExternalCreateSucceeded(o) + if t.IsZero() { + return false + } + return time.Since(t) < d +} + // AllowPropagation from one object to another by adding consenting annotations // to both. // Deprecated: This functionality will be removed soon. diff --git a/pkg/meta/meta_test.go b/pkg/meta/meta_test.go index a71fb798d..61e784d31 100644 --- a/pkg/meta/meta_test.go +++ b/pkg/meta/meta_test.go @@ -20,15 +20,16 @@ import ( "fmt" "hash/fnv" "testing" + "time" "github.com/google/go-cmp/cmp" - "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/test" ) @@ -901,6 +902,284 @@ func TestSetExternalName(t *testing.T) { } } +func TestGetExternalCreatePending(t *testing.T) { + now := time.Now().Round(time.Second) + + cases := map[string]struct { + o metav1.Object + want time.Time + }{ + "ExternalCreatePendingExists": { + o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreatePending: now.Format(time.RFC3339)}}}, + want: now, + }, + "NoExternalCreatePending": { + o: &corev1.Pod{}, + want: time.Time{}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := GetExternalCreatePending(tc.o) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("GetExternalCreatePending(...): -want, +got:\n%s", diff) + } + }) + } +} + +func TestSetExternalCreatePending(t *testing.T) { + now := time.Now() + + cases := map[string]struct { + o metav1.Object + t time.Time + want metav1.Object + }{ + "SetsTheCorrectKey": { + o: &corev1.Pod{}, + t: now, + want: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreatePending: now.Format(time.RFC3339)}}}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + SetExternalCreatePending(tc.o, tc.t) + if diff := cmp.Diff(tc.want, tc.o); diff != "" { + t.Errorf("SetExternalCreatePending(...): -want, +got:\n%s", diff) + } + }) + } +} + +func TestGetExternalCreateSucceeded(t *testing.T) { + now := time.Now().Round(time.Second) + + cases := map[string]struct { + o metav1.Object + want time.Time + }{ + "ExternalCreateTimeExists": { + o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateSucceeded: now.Format(time.RFC3339)}}}, + want: now, + }, + "NoExternalCreateTime": { + o: &corev1.Pod{}, + want: time.Time{}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := GetExternalCreateSucceeded(tc.o) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("GetExternalCreateSucceeded(...): -want, +got:\n%s", diff) + } + }) + } +} + +func TestSetExternalCreateSucceeded(t *testing.T) { + now := time.Now() + + cases := map[string]struct { + o metav1.Object + t time.Time + want metav1.Object + }{ + "SetsTheCorrectKey": { + o: &corev1.Pod{}, + t: now, + want: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateSucceeded: now.Format(time.RFC3339)}}}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + SetExternalCreateSucceeded(tc.o, tc.t) + if diff := cmp.Diff(tc.want, tc.o); diff != "" { + t.Errorf("SetExternalCreateSucceeded(...): -want, +got:\n%s", diff) + } + }) + } +} + +func TestGetExternalCreateFailed(t *testing.T) { + now := time.Now().Round(time.Second) + + cases := map[string]struct { + o metav1.Object + want time.Time + }{ + "ExternalCreateFailedExists": { + o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateFailed: now.Format(time.RFC3339)}}}, + want: now, + }, + "NoExternalCreateFailed": { + o: &corev1.Pod{}, + want: time.Time{}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := GetExternalCreateFailed(tc.o) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("GetExternalCreateFailed(...): -want, +got:\n%s", diff) + } + }) + } +} + +func TestSetExternalCreateFailed(t *testing.T) { + now := time.Now() + + cases := map[string]struct { + o metav1.Object + t time.Time + want metav1.Object + }{ + "SetsTheCorrectKey": { + o: &corev1.Pod{}, + t: now, + want: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateFailed: now.Format(time.RFC3339)}}}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + SetExternalCreateFailed(tc.o, tc.t) + if diff := cmp.Diff(tc.want, tc.o); diff != "" { + t.Errorf("SetExternalCreateFailed(...): -want, +got:\n%s", diff) + } + }) + } +} + +func TestExternalCreateSucceededDuring(t *testing.T) { + type args struct { + o metav1.Object + d time.Duration + } + + cases := map[string]struct { + args args + want bool + }{ + "NotYetSuccessfullyCreated": { + args: args{ + o: &corev1.Pod{}, + d: 1 * time.Minute, + }, + want: false, + }, + "SuccessfullyCreatedTooLongAgo": { + args: args{ + o: func() metav1.Object { + o := &corev1.Pod{} + t := time.Now().Add(-2 * time.Minute) + SetExternalCreateSucceeded(o, t) + return o + }(), + d: 1 * time.Minute, + }, + want: false, + }, + "SuccessfullyCreatedWithinDuration": { + args: args{ + o: func() metav1.Object { + o := &corev1.Pod{} + t := time.Now().Add(-30 * time.Second) + SetExternalCreateSucceeded(o, t) + return o + }(), + d: 1 * time.Minute, + }, + want: true, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := ExternalCreateSucceededDuring(tc.args.o, tc.args.d) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("ExternalCreateSucceededDuring(...): -want, +got:\n%s", diff) + } + }) + } +} + +func TestExternalCreateIncomplete(t *testing.T) { + + now := time.Now().Format(time.RFC3339) + earlier := time.Now().Add(-1 * time.Second).Format(time.RFC3339) + evenEarlier := time.Now().Add(-1 * time.Minute).Format(time.RFC3339) + + cases := map[string]struct { + reason string + o metav1.Object + want bool + }{ + "CreateNeverPending": { + reason: "If we've never called Create it can't be incomplete.", + o: &corev1.Pod{}, + want: false, + }, + "CreateSucceeded": { + reason: "If Create succeeded since it was pending, it's complete.", + o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ + AnnotationKeyExternalCreateFailed: evenEarlier, + AnnotationKeyExternalCreatePending: earlier, + AnnotationKeyExternalCreateSucceeded: now, + }}}, + want: false, + }, + "CreateFailed": { + reason: "If Create failed since it was pending, it's complete.", + o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ + AnnotationKeyExternalCreateSucceeded: evenEarlier, + AnnotationKeyExternalCreatePending: earlier, + AnnotationKeyExternalCreateFailed: now, + }}}, + want: false, + }, + "CreateNeverCompleted": { + reason: "If Create was pending but never succeeded or failed, it's incomplete.", + o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ + AnnotationKeyExternalCreatePending: earlier, + }}}, + want: true, + }, + "RecreateNeverCompleted": { + reason: "If Create is pending and there's an older success we're probably trying to recreate a deleted external resource, and it's incomplete.", + o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ + AnnotationKeyExternalCreateSucceeded: earlier, + AnnotationKeyExternalCreatePending: now, + }}}, + want: true, + }, + "RetryNeverCompleted": { + reason: "If Create is pending and there's an older failure we're probably trying to recreate a deleted external resource, and it's incomplete.", + o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ + AnnotationKeyExternalCreateFailed: earlier, + AnnotationKeyExternalCreatePending: now, + }}}, + want: true, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := ExternalCreateIncomplete(tc.o) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("ExternalCreateIncomplete(...): -want, +got:\n%s", diff) + } + }) + } +} + func TestAllowPropagation(t *testing.T) { fromns := "from-namespace" from := "from-name" diff --git a/pkg/parser/linter.go b/pkg/parser/linter.go index ea2c27c0c..a9fdce09b 100644 --- a/pkg/parser/linter.go +++ b/pkg/parser/linter.go @@ -17,14 +17,17 @@ limitations under the License. package parser import ( - "github.com/pkg/errors" + "strings" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/crossplane/crossplane-runtime/pkg/errors" ) const ( errNilLinterFn = "linter function is nil" - errOrFmt = "object did not pass either check: (%v), (%v)" + errOrFmt = "object did not pass any of the linters with following errors: %s" ) // A Linter lints packages. @@ -94,16 +97,19 @@ func (l *PackageLinter) Lint(pkg *Package) error { // Or checks that at least one of the passed linter functions does not return an // error. -func Or(a, b ObjectLinterFn) ObjectLinterFn { +func Or(linters ...ObjectLinterFn) ObjectLinterFn { return func(o runtime.Object) error { - if a == nil || b == nil { - return errors.New(errNilLinterFn) - } - aErr := a(o) - bErr := b(o) - if aErr == nil || bErr == nil { - return nil + var errs []string + for _, l := range linters { + if l == nil { + return errors.New(errNilLinterFn) + } + err := l(o) + if err == nil { + return nil + } + errs = append(errs, err.Error()) } - return errors.Errorf(errOrFmt, aErr, bErr) + return errors.Errorf(errOrFmt, strings.Join(errs, ", ")) } } diff --git a/pkg/parser/linter_test.go b/pkg/parser/linter_test.go index d929b4dc0..10e640490 100644 --- a/pkg/parser/linter_test.go +++ b/pkg/parser/linter_test.go @@ -20,9 +20,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/pkg/errors" "k8s.io/apimachinery/pkg/runtime" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/test" ) @@ -112,7 +112,7 @@ func TestLinter(t *testing.T) { objects: []runtime.Object{crd}, }, }, - err: errors.Errorf(errOrFmt, errBoom, errBoom), + err: errors.Errorf(errOrFmt, errBoom.Error()+", "+errBoom.Error()), }, } @@ -160,7 +160,7 @@ func TestOr(t *testing.T) { one: objFail, two: objFail, }, - err: errors.Errorf(errOrFmt, errBoom, errBoom), + err: errors.Errorf(errOrFmt, errBoom.Error()+", "+errBoom.Error()), }, "ErrNilLinter": { reason: "Passing a nil linter will should always return error.", diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index 97bbf2b85..e13a8110c 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -19,19 +19,19 @@ package parser import ( "bufio" "context" - "fmt" "io" "io/ioutil" "strings" "unicode" - "github.com/pkg/errors" "github.com/spf13/afero" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/kubernetes" + + "github.com/crossplane/crossplane-runtime/pkg/errors" ) // AnnotatedReadCloser is a wrapper around io.ReadCloser that allows @@ -111,26 +111,19 @@ func (p *PackageParser) Parse(ctx context.Context, reader io.ReadCloser) (*Packa if len(bytes) == 0 { continue } + if isWhiteSpace(bytes) { + continue + } m, _, err := dm.Decode(bytes, nil, nil) if err != nil { + // NOTE(hasheddan): we only try to decode with object scheme if the + // error is due the object not being registered in the meta scheme. + if !runtime.IsNotRegisteredError(err) { + return pkg, annotateErr(err, reader) + } o, _, err := do.Decode(bytes, nil, nil) if err != nil { - empty := true - for _, b := range bytes { - if !unicode.IsSpace(rune(b)) { - empty = false - break - } - } - // If the YAML document only contains Unicode White Space we - // ignore and do not return an error. - if empty { - continue - } - if anno, ok := reader.(AnnotatedReadCloser); ok { - return pkg, errors.Wrap(err, fmt.Sprintf("%+v", anno.Annotate())) - } - return pkg, err + return pkg, annotateErr(err, reader) } pkg.objects = append(pkg.objects, o) continue @@ -140,6 +133,27 @@ func (p *PackageParser) Parse(ctx context.Context, reader io.ReadCloser) (*Packa return pkg, nil } +// isWhiteSpace determines whether the passed in bytes are all unicode white +// space. +func isWhiteSpace(bytes []byte) bool { + empty := true + for _, b := range bytes { + if !unicode.IsSpace(rune(b)) { + empty = false + break + } + } + return empty +} + +// annotateErr annotates an error if the reader is an AnnotatedReadCloser. +func annotateErr(err error, reader io.ReadCloser) error { + if anno, ok := reader.(AnnotatedReadCloser); ok { + return errors.Wrapf(err, "%+v", anno.Annotate()) + } + return err +} + // BackendOption modifies the parser backend. Backends may accept options at // creation time, but must accept them at initialization. type BackendOption func(Backend) diff --git a/pkg/ratelimiter/default.go b/pkg/ratelimiter/default.go index cd8471f32..fad21921a 100644 --- a/pkg/ratelimiter/default.go +++ b/pkg/ratelimiter/default.go @@ -14,34 +14,70 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package ratelimiter contains suggested default ratelimiters for Crossplane. package ratelimiter import ( "time" "golang.org/x/time/rate" + "k8s.io/client-go/rest" "k8s.io/client-go/util/workqueue" "sigs.k8s.io/controller-runtime/pkg/ratelimiter" ) -// DefaultProviderRPS is the recommended default average requeues per second -// tolerated by a provider's controller manager. -const DefaultProviderRPS = 1 +const ( + // DefaultProviderRPS is the recommended default average requeues per + // second tolerated by a Crossplane provider. + // + // Deprecated. Use a flag. + DefaultProviderRPS = 1 +) + +// NewGlobal returns a token bucket rate limiter meant for limiting the number +// of average total requeues per second for all controllers registered with a +// controller manager. The bucket size (i.e. allowed burst) is rps * 10. +func NewGlobal(rps int) *workqueue.BucketRateLimiter { + return &workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(rps), rps*10)} +} + +// NewController returns a rate limiter that takes the maximum delay between the +// passed rate limiter and a per-item exponential backoff limiter. The +// exponential backoff limiter has a base delay of 1s and a maximum of 60s. +func NewController() ratelimiter.RateLimiter { + return workqueue.NewItemExponentialFailureRateLimiter(1*time.Second, 60*time.Second) +} // NewDefaultProviderRateLimiter returns a token bucket rate limiter meant for // limiting the number of average total requeues per second for all controllers // registered with a controller manager. The bucket size is a linear function of // the requeues per second. +// +// Deprecated: Use NewGlobal. func NewDefaultProviderRateLimiter(rps int) *workqueue.BucketRateLimiter { - return &workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(rps), rps*10)} + return NewGlobal(rps) } // NewDefaultManagedRateLimiter returns a rate limiter that takes the maximum // delay between the passed provider and a per-item exponential backoff limiter. // The exponential backoff limiter has a base delay of 1s and a maximum of 60s. +// +// Deprecated: Use NewController. func NewDefaultManagedRateLimiter(provider ratelimiter.RateLimiter) ratelimiter.RateLimiter { return workqueue.NewMaxOfRateLimiter( workqueue.NewItemExponentialFailureRateLimiter(1*time.Second, 60*time.Second), provider, ) } + +// LimitRESTConfig returns a copy of the supplied REST config with rate limits +// derived from the supplied rate of reconciles per second. +func LimitRESTConfig(cfg *rest.Config, rps int) *rest.Config { + // The Kubernetes controller manager and controller-runtime controller + // managers use 20qps with 30 burst. We default to 10 reconciles per + // second so our defaults are designed to accommodate that. + out := rest.CopyConfig(cfg) + out.QPS = float32(rps * 2) + out.Burst = 3 + return out +} diff --git a/pkg/ratelimiter/default_test.go b/pkg/ratelimiter/default_test.go index ceab248fe..e80bfebb4 100644 --- a/pkg/ratelimiter/default_test.go +++ b/pkg/ratelimiter/default_test.go @@ -21,7 +21,7 @@ import ( "time" ) -func TestDefaultMangedRateLimiter(t *testing.T) { +func TestDefaultManagedRateLimiter(t *testing.T) { limiter := NewDefaultManagedRateLimiter(NewDefaultProviderRateLimiter(DefaultProviderRPS)) backoffSchedule := []int{1, 2, 4, 8, 16, 32, 60} for _, d := range backoffSchedule { diff --git a/pkg/ratelimiter/reconciler.go b/pkg/ratelimiter/reconciler.go new file mode 100644 index 000000000..cb95315c2 --- /dev/null +++ b/pkg/ratelimiter/reconciler.go @@ -0,0 +1,89 @@ +/* +Copyright 2021 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 ratelimiter + +import ( + "context" + "sync" + "time" + + "sigs.k8s.io/controller-runtime/pkg/ratelimiter" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// A Reconciler rate limits an inner, wrapped Reconciler. Requests that are rate +// limited immediately return RequeueAfter: d without calling the wrapped +// Reconciler, where d is imposed by the rate limiter. +type Reconciler struct { + name string + inner reconcile.Reconciler + limit ratelimiter.RateLimiter + + limited map[string]struct{} + limitedL sync.RWMutex +} + +// NewReconciler wraps the supplied Reconciler, ensuring requests are passed to +// it no more frequently than the supplied RateLimiter allows. Multiple uniquely +// named Reconcilers can share the same RateLimiter. +func NewReconciler(name string, r reconcile.Reconciler, l ratelimiter.RateLimiter) *Reconciler { + return &Reconciler{name: name, inner: r, limit: l, limited: make(map[string]struct{})} +} + +// Reconcile the supplied request subject to rate limiting. +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + item := r.name + req.String() + if d := r.when(req); d > 0 { + return reconcile.Result{RequeueAfter: d}, nil + } + r.limit.Forget(item) + return r.inner.Reconcile(ctx, req) +} + +// when adapts the upstream rate limiter's 'When' method such that rate limited +// requests can call it again when they return and will be allowed to proceed +// immediately without being subject to further rate limiting. It is optimised +// for handling requests that have not been and will not be rate limited without +// blocking. +func (r *Reconciler) when(req reconcile.Request) time.Duration { + item := r.name + req.String() + + r.limitedL.RLock() + _, limited := r.limited[item] + r.limitedL.RUnlock() + + // If we already rate limited this request we trust that it complied and + // let it pass immediately. + if limited { + r.limitedL.Lock() + delete(r.limited, item) + r.limitedL.Unlock() + return 0 + } + + d := r.limit.When(item) + + // Record that this request was rate limited so that we can let it + // through immediately when it requeues after the supplied duration. + if d != 0 { + r.limitedL.Lock() + r.limited[item] = struct{}{} + r.limitedL.Unlock() + } + + return d +} diff --git a/pkg/ratelimiter/reconciler_test.go b/pkg/ratelimiter/reconciler_test.go new file mode 100644 index 000000000..e9f6eb426 --- /dev/null +++ b/pkg/ratelimiter/reconciler_test.go @@ -0,0 +1,111 @@ +/* +Copyright 2021 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 ratelimiter + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/ratelimiter" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/crossplane/crossplane-runtime/pkg/test" +) + +var _ ratelimiter.RateLimiter = &predictableRateLimiter{} + +type predictableRateLimiter struct{ d time.Duration } + +func (r *predictableRateLimiter) When(_ interface{}) time.Duration { return r.d } +func (r *predictableRateLimiter) Forget(_ interface{}) {} +func (r *predictableRateLimiter) NumRequeues(_ interface{}) int { return 0 } + +func TestReconcile(t *testing.T) { + type args struct { + ctx context.Context + req reconcile.Request + } + type want struct { + res reconcile.Result + err error + } + + cases := map[string]struct { + reason string + r reconcile.Reconciler + args args + want want + }{ + "NotRateLimited": { + reason: "Requests that are not rate limited should be passed to the inner Reconciler.", + r: NewReconciler("test", + reconcile.Func(func(c context.Context, r reconcile.Request) (reconcile.Result, error) { + return reconcile.Result{Requeue: true}, nil + }), + &predictableRateLimiter{}), + want: want{ + res: reconcile.Result{Requeue: true}, + err: nil, + }, + }, + "RateLimited": { + reason: "Requests that are rate limited should be requeued after the duration specified by the RateLimiter.", + r: NewReconciler("test", nil, &predictableRateLimiter{d: 8 * time.Second}), + want: want{ + res: reconcile.Result{RequeueAfter: 8 * time.Second}, + err: nil, + }, + }, + "Returning": { + reason: "Returning requests that were previously rate limited should be allowed through without further rate limiting.", + r: func() reconcile.Reconciler { + inner := reconcile.Func(func(c context.Context, r reconcile.Request) (reconcile.Result, error) { + return reconcile.Result{Requeue: true}, nil + }) + + // Rate limit the request once. + r := NewReconciler("test", inner, &predictableRateLimiter{d: 8 * time.Second}) + r.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "limited"}}) + return r + + }(), + args: args{ + ctx: context.Background(), + req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "limited"}}, + }, + want: want{ + res: reconcile.Result{Requeue: true}, + err: nil, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got, err := tc.r.Reconcile(tc.args.ctx, tc.args.req) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("%s\nr.Reconcile(...): -want, +got error:\n%s", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.res, got); diff != "" { + t.Errorf("%s\nr.Reconcile(...): -want, +got result:\n%s", tc.reason, diff) + } + }) + } +} diff --git a/pkg/reconciler/managed/api.go b/pkg/reconciler/managed/api.go index 6dc1af9d9..dcde6825a 100644 --- a/pkg/reconciler/managed/api.go +++ b/pkg/reconciler/managed/api.go @@ -20,21 +20,26 @@ import ( "context" "github.com/google/go-cmp/cmp" - "github.com/pkg/errors" + "github.com/google/go-cmp/cmp/cmpopts" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/crossplane/crossplane-runtime/pkg/resource" ) // Error strings. const ( - errCreateOrUpdateSecret = "cannot create or update connection secret" - errUpdateManaged = "cannot update managed resource" - errUpdateManagedStatus = "cannot update managed resource status" - errResolveReferences = "cannot resolve references" + errCreateOrUpdateSecret = "cannot create or update connection secret" + errUpdateManaged = "cannot update managed resource" + errUpdateManagedStatus = "cannot update managed resource status" + errResolveReferences = "cannot resolve references" + errUpdateCriticalAnnotations = "cannot update critical annotations" ) // NameAsExternalName writes the name of the managed resource to @@ -97,21 +102,37 @@ func NewAPISecretPublisher(c client.Client, ot runtime.ObjectTyper) *APISecretPu // PublishConnection publishes the supplied ConnectionDetails to a Secret in the // same namespace as the supplied Managed resource. It is a no-op if the secret // already exists with the supplied ConnectionDetails. -func (a *APISecretPublisher) PublishConnection(ctx context.Context, mg resource.Managed, c ConnectionDetails) error { +func (a *APISecretPublisher) PublishConnection(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) (bool, error) { // This resource does not want to expose a connection secret. - if mg.GetWriteConnectionSecretToReference() == nil { - return nil + if o.GetWriteConnectionSecretToReference() == nil { + return false, nil } - s := resource.ConnectionSecretFor(mg, resource.MustGetKind(mg, a.typer)) + s := resource.ConnectionSecretFor(o, resource.MustGetKind(o, a.typer)) s.Data = c - return errors.Wrap(a.secret.Apply(ctx, s, resource.ConnectionSecretMustBeControllableBy(mg.GetUID())), errCreateOrUpdateSecret) + err := a.secret.Apply(ctx, s, + resource.ConnectionSecretMustBeControllableBy(o.GetUID()), + resource.AllowUpdateIf(func(current, desired runtime.Object) bool { + // We consider the update to be a no-op and don't allow it if the + // current and existing secret data are identical. + return !cmp.Equal(current.(*corev1.Secret).Data, desired.(*corev1.Secret).Data, cmpopts.EquateEmpty()) + }), + ) + if resource.IsNotAllowed(err) { + // The update was not allowed because it was a no-op. + return false, nil + } + if err != nil { + return false, errors.Wrap(err, errCreateOrUpdateSecret) + } + + return true, nil } // UnpublishConnection is no-op since PublishConnection only creates resources // that will be garbage collected by Kubernetes when the managed resource is // deleted. -func (a *APISecretPublisher) UnpublishConnection(ctx context.Context, mg resource.Managed, c ConnectionDetails) error { +func (a *APISecretPublisher) UnpublishConnection(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) error { return nil } @@ -152,3 +173,33 @@ func (a *APISimpleReferenceResolver) ResolveReferences(ctx context.Context, mg r return errors.Wrap(a.client.Update(ctx, mg), errUpdateManaged) } + +// A RetryingCriticalAnnotationUpdater is a CriticalAnnotationUpdater that +// retries annotation updates in the face of API server errors. +type RetryingCriticalAnnotationUpdater struct { + client client.Client +} + +// NewRetryingCriticalAnnotationUpdater returns a CriticalAnnotationUpdater that +// retries annotation updates in the face of API server errors. +func NewRetryingCriticalAnnotationUpdater(c client.Client) *RetryingCriticalAnnotationUpdater { + return &RetryingCriticalAnnotationUpdater{client: c} +} + +// UpdateCriticalAnnotations updates (i.e. persists) the annotations of the +// supplied Object. It retries in the face of any API server error several times +// in order to ensure annotations that contain critical state are persisted. Any +// pending changes to the supplied Object's spec, status, or other metadata are +// reset to their current state according to the API server. +func (u *RetryingCriticalAnnotationUpdater) UpdateCriticalAnnotations(ctx context.Context, o client.Object) error { + a := o.GetAnnotations() + err := retry.OnError(retry.DefaultRetry, resource.IsAPIError, func() error { + nn := types.NamespacedName{Name: o.GetName()} + if err := u.client.Get(ctx, nn, o); err != nil { + return err + } + meta.AddAnnotations(o, a) + return u.client.Update(ctx, o) + }) + return errors.Wrap(err, errUpdateCriticalAnnotations) +} diff --git a/pkg/reconciler/managed/api_test.go b/pkg/reconciler/managed/api_test.go index f89da6cb6..1ea4f8389 100644 --- a/pkg/reconciler/managed/api_test.go +++ b/pkg/reconciler/managed/api_test.go @@ -21,12 +21,12 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/crossplane/crossplane-runtime/pkg/resource" "github.com/crossplane/crossplane-runtime/pkg/resource/fake" @@ -49,7 +49,8 @@ func TestNameAsExternalName(t *testing.T) { } errBoom := errors.New("boom") - testExternalName := "my-external-name" + testExternalName := "my-" + + "external-name" cases := map[string]struct { client client.Client @@ -205,11 +206,15 @@ func TestAPISecretPublisher(t *testing.T) { c ConnectionDetails } + type want struct { + err error + published bool + } cases := map[string]struct { reason string fields fields args args - want error + want want }{ "ResourceDoesNotPublishSecret": { reason: "A managed resource with a nil GetWriteConnectionSecretToReference should not publish a secret", @@ -228,7 +233,34 @@ func TestAPISecretPublisher(t *testing.T) { ctx: context.Background(), mg: mg, }, - want: errors.Wrap(errBoom, errCreateOrUpdateSecret), + want: want{ + err: errors.Wrap(errBoom, errCreateOrUpdateSecret), + }, + }, + "AlreadyPublished": { + reason: "An up to date connection secret should result in no error and not being published", + fields: fields{ + secret: resource.ApplyFn(func(_ context.Context, o client.Object, ao ...resource.ApplyOption) error { + want := resource.ConnectionSecretFor(mg, fake.GVK(mg)) + want.Data = cd + for _, fn := range ao { + if err := fn(context.Background(), o, want); err != nil { + return err + } + } + return nil + }), + typer: fake.SchemeWith(&fake.Managed{}), + }, + args: args{ + ctx: context.Background(), + mg: mg, + c: cd, + }, + want: want{ + published: false, + err: nil, + }, }, "Success": { reason: "A successful application of the connection secret should result in no error", @@ -248,15 +280,21 @@ func TestAPISecretPublisher(t *testing.T) { mg: mg, c: cd, }, + want: want{ + published: true, + }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { a := &APISecretPublisher{tc.fields.secret, tc.fields.typer} - got := a.PublishConnection(tc.args.ctx, tc.args.mg, tc.args.c) - if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { - t.Errorf("\n%s\nPublish(...): -want, +got:\n%s", tc.reason, diff) + got, gotErr := a.PublishConnection(tc.args.ctx, tc.args.mg, tc.args.c) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nPublish(...): -wantErr, +gotErr:\n%s", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.published, got); diff != "" { + t.Errorf("\n%s\nPublish(...): -wantPublished, +gotPublished:\n%s", tc.reason, diff) } }) } @@ -377,7 +415,67 @@ func TestResolveReferences(t *testing.T) { r := NewAPISimpleReferenceResolver(tc.c) got := r.ResolveReferences(tc.args.ctx, tc.args.mg) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { - t.Errorf("\nReason: %s\r.ResolveReferences(...): -want, +got:\n%s", tc.reason, diff) + t.Errorf("\n%s\nr.ResolveReferences(...): -want, +got:\n%s", tc.reason, diff) + } + }) + } +} + +func TestRetryingCriticalAnnotationUpdater(t *testing.T) { + + errBoom := errors.New("boom") + + type args struct { + ctx context.Context + o client.Object + } + + cases := map[string]struct { + reason string + c client.Client + args args + want error + }{ + "GetError": { + reason: "We should return any error we encounter getting the supplied object", + c: &test.MockClient{ + MockGet: test.NewMockGetFn(errBoom), + }, + args: args{ + o: &fake.Managed{}, + }, + want: errors.Wrap(errBoom, errUpdateCriticalAnnotations), + }, + "UpdateError": { + reason: "We should return any error we encounter updating the supplied object", + c: &test.MockClient{ + MockGet: test.NewMockGetFn(nil), + MockUpdate: test.NewMockUpdateFn(errBoom), + }, + args: args{ + o: &fake.Managed{}, + }, + want: errors.Wrap(errBoom, errUpdateCriticalAnnotations), + }, + "Success": { + reason: "We should return without error if we successfully update our annotations", + c: &test.MockClient{ + MockGet: test.NewMockGetFn(nil), + MockUpdate: test.NewMockUpdateFn(errBoom), + }, + args: args{ + o: &fake.Managed{}, + }, + want: errors.Wrap(errBoom, errUpdateCriticalAnnotations), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + u := NewRetryingCriticalAnnotationUpdater(tc.c) + got := u.UpdateCriticalAnnotations(tc.args.ctx, tc.args.o) + if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nu.UpdateCriticalAnnotations(...): -want, +got:\n%s", tc.reason, diff) } }) } diff --git a/pkg/reconciler/managed/publisher.go b/pkg/reconciler/managed/publisher.go index 5c31a9fb0..07db0b841 100644 --- a/pkg/reconciler/managed/publisher.go +++ b/pkg/reconciler/managed/publisher.go @@ -19,30 +19,61 @@ package managed import ( "context" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/resource" ) +const errSecretStoreDisabled = "cannot publish to secret store, feature is not enabled" + // A PublisherChain chains multiple ManagedPublishers. type PublisherChain []ConnectionPublisher // PublishConnection calls each ConnectionPublisher.PublishConnection serially. It returns the first error it // encounters, if any. -func (pc PublisherChain) PublishConnection(ctx context.Context, mg resource.Managed, c ConnectionDetails) error { +func (pc PublisherChain) PublishConnection(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) (bool, error) { + published := false for _, p := range pc { - if err := p.PublishConnection(ctx, mg, c); err != nil { - return err + pb, err := p.PublishConnection(ctx, o, c) + if err != nil { + return published, err + } + if pb { + published = true } } - return nil + return published, nil } // UnpublishConnection calls each ConnectionPublisher.UnpublishConnection serially. It returns the first error it // encounters, if any. -func (pc PublisherChain) UnpublishConnection(ctx context.Context, mg resource.Managed, c ConnectionDetails) error { +func (pc PublisherChain) UnpublishConnection(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) error { for _, p := range pc { - if err := p.UnpublishConnection(ctx, mg, c); err != nil { + if err := p.UnpublishConnection(ctx, o, c); err != nil { return err } } return nil } + +// DisabledSecretStoreManager is a connection details manager that returns a proper +// error when API used but feature not enabled. +type DisabledSecretStoreManager struct { +} + +// PublishConnection returns a proper error when API used but the feature was +// not enabled. +func (m *DisabledSecretStoreManager) PublishConnection(_ context.Context, so resource.ConnectionSecretOwner, _ ConnectionDetails) (bool, error) { + if so.GetPublishConnectionDetailsTo() != nil { + return false, errors.New(errSecretStoreDisabled) + } + return false, nil +} + +// UnpublishConnection returns a proper error when API used but the feature was +// not enabled. +func (m *DisabledSecretStoreManager) UnpublishConnection(_ context.Context, so resource.ConnectionSecretOwner, _ ConnectionDetails) error { + if so.GetPublishConnectionDetailsTo() != nil { + return errors.New(errSecretStoreDisabled) + } + return nil +} diff --git a/pkg/reconciler/managed/publisher_test.go b/pkg/reconciler/managed/publisher_test.go index cf94ec012..928b8fde2 100644 --- a/pkg/reconciler/managed/publisher_test.go +++ b/pkg/reconciler/managed/publisher_test.go @@ -21,8 +21,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/pkg/errors" + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/resource" "github.com/crossplane/crossplane-runtime/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/pkg/test" @@ -40,12 +41,17 @@ func TestPublisherChain(t *testing.T) { c ConnectionDetails } + type want struct { + err error + published bool + } + errBoom := errors.New("boom") cases := map[string]struct { p ConnectionPublisher args args - want error + want want }{ "EmptyChain": { p: PublisherChain{}, @@ -54,15 +60,14 @@ func TestPublisherChain(t *testing.T) { mg: &fake.Managed{}, c: ConnectionDetails{}, }, - want: nil, }, "SuccessfulPublisher": { p: PublisherChain{ ConnectionPublisherFns{ - PublishConnectionFn: func(_ context.Context, mg resource.Managed, c ConnectionDetails) error { - return nil + PublishConnectionFn: func(_ context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) (bool, error) { + return true, nil }, - UnpublishConnectionFn: func(ctx context.Context, mg resource.Managed, c ConnectionDetails) error { + UnpublishConnectionFn: func(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) error { return nil }, }, @@ -72,15 +77,17 @@ func TestPublisherChain(t *testing.T) { mg: &fake.Managed{}, c: ConnectionDetails{}, }, - want: nil, + want: want{ + published: true, + }, }, "PublisherReturnsError": { p: PublisherChain{ ConnectionPublisherFns{ - PublishConnectionFn: func(_ context.Context, mg resource.Managed, c ConnectionDetails) error { - return errBoom + PublishConnectionFn: func(_ context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) (bool, error) { + return false, errBoom }, - UnpublishConnectionFn: func(ctx context.Context, mg resource.Managed, c ConnectionDetails) error { + UnpublishConnectionFn: func(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) error { return nil }, }, @@ -90,14 +97,107 @@ func TestPublisherChain(t *testing.T) { mg: &fake.Managed{}, c: ConnectionDetails{}, }, - want: errBoom, + want: want{ + err: errBoom, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got, gotErr := tc.p.PublishConnection(tc.args.ctx, tc.args.mg, tc.args.c) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Errorf("Publish(...): -want, +got:\n%s", diff) + } + if diff := cmp.Diff(tc.want.published, got); diff != "" { + t.Errorf("Publish(...): -wantPublished, +gotPublished:\n%s", diff) + } + }) + } +} + +func TestDisabledSecretStorePublish(t *testing.T) { + type args struct { + mg resource.Managed + } + type want struct { + published bool + err error + } + + cases := map[string]struct { + args args + want want + }{ + "APINotUsedNoError": { + args: args{ + mg: &fake.Managed{}, + }, + }, + "APIUsedProperError": { + args: args{ + mg: &fake.Managed{ + ConnectionDetailsPublisherTo: fake.ConnectionDetailsPublisherTo{ + To: &xpv1.PublishConnectionDetailsTo{}, + }, + }, + }, + want: want{ + err: errors.New(errSecretStoreDisabled), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + ss := &DisabledSecretStoreManager{} + got, gotErr := ss.PublishConnection(context.Background(), tc.args.mg, nil) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Errorf("Publish(...): -want, +got:\n%s", diff) + } + if diff := cmp.Diff(tc.want.published, got); diff != "" { + t.Errorf("Publish(...): -wantPublished, +gotPublished:\n%s", diff) + } + }) + } +} + +func TestDisabledSecretStoreUnpublish(t *testing.T) { + type args struct { + mg resource.Managed + } + type want struct { + err error + } + + cases := map[string]struct { + args args + want want + }{ + "APINotUsedNoError": { + args: args{ + mg: &fake.Managed{}, + }, + }, + "APIUsedProperError": { + args: args{ + mg: &fake.Managed{ + ConnectionDetailsPublisherTo: fake.ConnectionDetailsPublisherTo{ + To: &xpv1.PublishConnectionDetailsTo{}, + }, + }, + }, + want: want{ + err: errors.New(errSecretStoreDisabled), + }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { - got := tc.p.PublishConnection(tc.args.ctx, tc.args.mg, tc.args.c) - if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { + ss := &DisabledSecretStoreManager{} + gotErr := ss.UnpublishConnection(context.Background(), tc.args.mg, nil) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { t.Errorf("Publish(...): -want, +got:\n%s", diff) } }) diff --git a/pkg/reconciler/managed/reconciler.go b/pkg/reconciler/managed/reconciler.go index 382506a8e..ddc369bfb 100644 --- a/pkg/reconciler/managed/reconciler.go +++ b/pkg/reconciler/managed/reconciler.go @@ -21,16 +21,13 @@ import ( "strings" "time" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/retry" - - "github.com/pkg/errors" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/event" "github.com/crossplane/crossplane-runtime/pkg/logging" "github.com/crossplane/crossplane-runtime/pkg/meta" @@ -38,17 +35,22 @@ import ( ) const ( - managedFinalizerName = "finalizer.managedresource.crossplane.io" + // FinalizerName is the string that is used as finalizer on managed resource + // objects. + FinalizerName = "finalizer.managedresource.crossplane.io" + reconcileGracePeriod = 30 * time.Second reconcileTimeout = 1 * time.Minute defaultpollInterval = 1 * time.Minute + defaultGracePeriod = 30 * time.Second ) // Error strings. const ( errGetManaged = "cannot get managed resource" - errUpdateManagedAfterCreate = "cannot update managed resource. this may have resulted in a leaked external resource" + errUpdateManagedAnnotations = "cannot update managed resource annotations" + errCreateIncomplete = "cannot determine creation result - remove the " + meta.AnnotationKeyExternalCreatePending + " annotation if it is safe to proceed" errReconcileConnect = "connect failed" errReconcileObserve = "observe failed" errReconcileCreate = "create failed" @@ -59,6 +61,7 @@ const ( // Event reasons. const ( reasonCannotConnect event.Reason = "CannotConnectToProvider" + reasonCannotDisconnect event.Reason = "CannotDisconnectFromProvider" reasonCannotInitialize event.Reason = "CannotInitializeManagedResource" reasonCannotResolveRefs event.Reason = "CannotResolveResourceReferences" reasonCannotObserve event.Reason = "CannotObserveExternalResource" @@ -72,6 +75,7 @@ const ( reasonDeleted event.Reason = "DeletedExternalResource" reasonCreated event.Reason = "CreatedExternalResource" reasonUpdated event.Reason = "UpdatedExternalResource" + reasonPending event.Reason = "PendingExternalResource" ) // ControllerName returns the recommended name for controllers that use this @@ -80,6 +84,21 @@ func ControllerName(kind string) string { return "managed/" + strings.ToLower(kind) } +// A CriticalAnnotationUpdater is used when it is critical that annotations must +// be updated before returning from the Reconcile loop. +type CriticalAnnotationUpdater interface { + UpdateCriticalAnnotations(ctx context.Context, o client.Object) error +} + +// A CriticalAnnotationUpdateFn may be used when it is critical that annotations +// must be updated before returning from the Reconcile loop. +type CriticalAnnotationUpdateFn func(ctx context.Context, o client.Object) error + +// UpdateCriticalAnnotations of the supplied object. +func (fn CriticalAnnotationUpdateFn) UpdateCriticalAnnotations(ctx context.Context, o client.Object) error { + return fn(ctx, o) +} + // ConnectionDetails created or updated during an operation on an external // resource, for example usernames, passwords, endpoints, ports, etc. type ConnectionDetails map[string][]byte @@ -91,26 +110,32 @@ type ConnectionPublisher interface { // PublishConnection details for the supplied Managed resource. Publishing // must be additive; i.e. if details (a, b, c) are published, subsequently // publicing details (b, c, d) should update (b, c) but not remove a. - PublishConnection(ctx context.Context, mg resource.Managed, c ConnectionDetails) error + PublishConnection(ctx context.Context, so resource.ConnectionSecretOwner, c ConnectionDetails) (published bool, err error) // UnpublishConnection details for the supplied Managed resource. - UnpublishConnection(ctx context.Context, mg resource.Managed, c ConnectionDetails) error + UnpublishConnection(ctx context.Context, so resource.ConnectionSecretOwner, c ConnectionDetails) error } // ConnectionPublisherFns is the pluggable struct to produce objects with ConnectionPublisher interface. type ConnectionPublisherFns struct { - PublishConnectionFn func(ctx context.Context, mg resource.Managed, c ConnectionDetails) error - UnpublishConnectionFn func(ctx context.Context, mg resource.Managed, c ConnectionDetails) error + PublishConnectionFn func(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) (bool, error) + UnpublishConnectionFn func(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) error } // PublishConnection details for the supplied Managed resource. -func (fn ConnectionPublisherFns) PublishConnection(ctx context.Context, mg resource.Managed, c ConnectionDetails) error { - return fn.PublishConnectionFn(ctx, mg, c) +func (fn ConnectionPublisherFns) PublishConnection(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) (bool, error) { + return fn.PublishConnectionFn(ctx, o, c) } // UnpublishConnection details for the supplied Managed resource. -func (fn ConnectionPublisherFns) UnpublishConnection(ctx context.Context, mg resource.Managed, c ConnectionDetails) error { - return fn.UnpublishConnectionFn(ctx, mg, c) +func (fn ConnectionPublisherFns) UnpublishConnection(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) error { + return fn.UnpublishConnectionFn(ctx, o, c) +} + +// A ConnectionDetailsFetcher fetches connection details for the supplied +// Connection Secret owner. +type ConnectionDetailsFetcher interface { + FetchConnection(ctx context.Context, so resource.ConnectionSecretOwner) (ConnectionDetails, error) } // A Initializer establishes ownership of the supplied Managed resource. @@ -169,6 +194,41 @@ type ExternalConnecter interface { Connect(ctx context.Context, mg resource.Managed) (ExternalClient, error) } +// An ExternalDisconnecter disconnects from a provider. +type ExternalDisconnecter interface { + // Disconnect from the provider and close the ExternalClient. + Disconnect(ctx context.Context) error +} + +// A NopDisconnecter converts an ExternalConnecter into an +// ExternalConnectDisconnecter with a no-op Disconnect method. +type NopDisconnecter struct { + c ExternalConnecter +} + +// Connect calls the underlying ExternalConnecter's Connect method. +func (c *NopDisconnecter) Connect(ctx context.Context, mg resource.Managed) (ExternalClient, error) { + return c.c.Connect(ctx, mg) +} + +// Disconnect does nothing. It never returns an error. +func (c *NopDisconnecter) Disconnect(_ context.Context) error { + return nil +} + +// NewNopDisconnecter converts an ExternalConnecter into an +// ExternalConnectDisconnecter with a no-op Disconnect method. +func NewNopDisconnecter(c ExternalConnecter) ExternalConnectDisconnecter { + return &NopDisconnecter{c} +} + +// An ExternalConnectDisconnecter produces a new ExternalClient given the supplied +// Managed resource. +type ExternalConnectDisconnecter interface { + ExternalConnecter + ExternalDisconnecter +} + // An ExternalConnectorFn is a function that satisfies the ExternalConnecter // interface. type ExternalConnectorFn func(ctx context.Context, mg resource.Managed) (ExternalClient, error) @@ -179,21 +239,52 @@ func (ec ExternalConnectorFn) Connect(ctx context.Context, mg resource.Managed) return ec(ctx, mg) } +// An ExternalDisconnectorFn is a function that satisfies the ExternalConnecter +// interface. +type ExternalDisconnectorFn func(ctx context.Context) error + +// Disconnect from provider and close the ExternalClient. +func (ed ExternalDisconnectorFn) Disconnect(ctx context.Context) error { + return ed(ctx) +} + +// ExternalConnectDisconnecterFns are functions that satisfy the +// ExternalConnectDisconnecter interface. +type ExternalConnectDisconnecterFns struct { + ConnectFn func(ctx context.Context, mg resource.Managed) (ExternalClient, error) + DisconnectFn func(ctx context.Context) error +} + +// Connect to the provider specified by the supplied managed resource and +// produce an ExternalClient. +func (fns ExternalConnectDisconnecterFns) Connect(ctx context.Context, mg resource.Managed) (ExternalClient, error) { + return fns.ConnectFn(ctx, mg) +} + +// Disconnect from the provider and close the ExternalClient. +func (fns ExternalConnectDisconnecterFns) Disconnect(ctx context.Context) error { + return fns.DisconnectFn(ctx) +} + // An ExternalClient manages the lifecycle of an external resource. // None of the calls here should be blocking. All of the calls should be // idempotent. For example, Create call should not return AlreadyExists error // if it's called again with the same parameters or Delete call should not // return error if there is an ongoing deletion or resource does not exist. type ExternalClient interface { - // Observe the external resource the supplied Managed resource represents, - // if any. Observe implementations must not modify the external resource, - // but may update the supplied Managed resource to reflect the state of the - // external resource. + // Observe the external resource the supplied Managed resource + // represents, if any. Observe implementations must not modify the + // external resource, but may update the supplied Managed resource to + // reflect the state of the external resource. Status modifications are + // automatically persisted unless ResourceLateInitialized is true - see + // ResourceLateInitialized for more detail. Observe(ctx context.Context, mg resource.Managed) (ExternalObservation, error) // Create an external resource per the specifications of the supplied // Managed resource. Called when Observe reports that the associated - // external resource does not exist. + // external resource does not exist. Create implementations may update + // managed resource annotations, and those updates will be persisted. + // All other updates will be discarded. Create(ctx context.Context, mg resource.Managed) (ExternalCreation, error) // Update the external resource represented by the supplied Managed @@ -316,10 +407,14 @@ type ExternalObservation struct { // An ExternalCreation is the result of the creation of an external resource. type ExternalCreation struct { - // ExternalNameAssigned is true if the Create operation resulted in a change - // in the external name annotation. If that's the case, we need to issue a - // spec update and make sure it goes through so that we don't lose the identifier - // of the resource we just created. + // ExternalNameAssigned should be true if the Create operation resulted + // in a change in the resource's external name. This is typically only + // needed for external resource's with unpredictable external names that + // are returned from the API at create time. + // + // Deprecated: The managed.Reconciler no longer needs to be informed + // when an external name is assigned by the Create operation. It will + // automatically detect and handle external name assignment. ExternalNameAssigned bool // ConnectionDetails required to connect to this resource. These details @@ -348,8 +443,9 @@ type Reconciler struct { client client.Client newManaged func() resource.Managed - pollInterval time.Duration - timeout time.Duration + pollInterval time.Duration + timeout time.Duration + creationGracePeriod time.Duration // The below structs embed the set of interfaces used to implement the // managed resource reconciler. We do this primarily for readability, so @@ -363,6 +459,7 @@ type Reconciler struct { } type mrManaged struct { + CriticalAnnotationUpdater ConnectionPublisher resource.Finalizer Initializer @@ -371,20 +468,24 @@ type mrManaged struct { func defaultMRManaged(m manager.Manager) mrManaged { return mrManaged{ - ConnectionPublisher: NewAPISecretPublisher(m.GetClient(), m.GetScheme()), - Finalizer: resource.NewAPIFinalizer(m.GetClient(), managedFinalizerName), - Initializer: NewNameAsExternalName(m.GetClient()), - ReferenceResolver: NewAPISimpleReferenceResolver(m.GetClient()), + CriticalAnnotationUpdater: NewRetryingCriticalAnnotationUpdater(m.GetClient()), + Finalizer: resource.NewAPIFinalizer(m.GetClient(), FinalizerName), + Initializer: NewNameAsExternalName(m.GetClient()), + ReferenceResolver: NewAPISimpleReferenceResolver(m.GetClient()), + ConnectionPublisher: PublisherChain([]ConnectionPublisher{ + NewAPISecretPublisher(m.GetClient(), m.GetScheme()), + &DisabledSecretStoreManager{}, + }), } } type mrExternal struct { - ExternalConnecter + ExternalConnectDisconnecter } func defaultMRExternal() mrExternal { return mrExternal{ - ExternalConnecter: &NopConnecter{}, + ExternalConnectDisconnecter: NewNopDisconnecter(&NopConnecter{}), } } @@ -412,11 +513,40 @@ func WithPollInterval(after time.Duration) ReconcilerOption { } } +// WithCreationGracePeriod configures an optional period during which we will +// wait for the external API to report that a newly created external resource +// exists. This allows us to tolerate eventually consistent APIs that do not +// immediately report that newly created resources exist when queried. All +// resources have a 30 second grace period by default. +func WithCreationGracePeriod(d time.Duration) ReconcilerOption { + return func(r *Reconciler) { + r.creationGracePeriod = d + } +} + // WithExternalConnecter specifies how the Reconciler should connect to the API // used to sync and delete external resources. func WithExternalConnecter(c ExternalConnecter) ReconcilerOption { return func(r *Reconciler) { - r.external.ExternalConnecter = c + r.external.ExternalConnectDisconnecter = NewNopDisconnecter(c) + } +} + +// WithExternalConnectDisconnecter specifies how the Reconciler should connect and disconnect to the API +// used to sync and delete external resources. +func WithExternalConnectDisconnecter(c ExternalConnectDisconnecter) ReconcilerOption { + return func(r *Reconciler) { + r.external.ExternalConnectDisconnecter = c + } +} + +// WithCriticalAnnotationUpdater specifies how the Reconciler should update a +// managed resource's critical annotations. Implementations typically contain +// some kind of retry logic to increase the likelihood that critical annotations +// (like non-deterministic external names) will be persisted. +func WithCriticalAnnotationUpdater(u CriticalAnnotationUpdater) ReconcilerOption { + return func(r *Reconciler) { + r.managed.CriticalAnnotationUpdater = u } } @@ -483,14 +613,15 @@ func NewReconciler(m manager.Manager, of resource.ManagedKind, o ...ReconcilerOp _ = nm() r := &Reconciler{ - client: m.GetClient(), - newManaged: nm, - pollInterval: defaultpollInterval, - timeout: reconcileTimeout, - managed: defaultMRManaged(m), - external: defaultMRExternal(), - log: logging.NewNopLogger(), - record: event.NewNopRecorder(), + client: m.GetClient(), + newManaged: nm, + pollInterval: defaultpollInterval, + creationGracePeriod: defaultGracePeriod, + timeout: reconcileTimeout, + managed: defaultMRManaged(m), + external: defaultMRExternal(), + log: logging.NewNopLogger(), + record: event.NewNopRecorder(), } for _, ro := range o { @@ -501,14 +632,14 @@ func NewReconciler(m manager.Manager, of resource.ManagedKind, o ...ReconcilerOp } // Reconcile a managed resource with an external resource. -func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconcile.Result, error) { // nolint:gocyclo +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { // nolint:gocyclo // NOTE(negz): This method is a well over our cyclomatic complexity goal. // Be wary of adding additional complexity. log := r.log.WithValues("request", req) log.Debug("Reconciling") - ctx, cancel := context.WithTimeout(context.Background(), r.timeout+reconcileGracePeriod) + ctx, cancel := context.WithTimeout(ctx, r.timeout+reconcileGracePeriod) defer cancel() // Govet linter has a check for lost cancel funcs but it's a false positive @@ -536,7 +667,6 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc // to unpublish connection details and remove finalizer. if meta.WasDeleted(managed) && managed.GetDeletionPolicy() == xpv1.DeletionOrphan { log = log.WithValues("deletion-timestamp", managed.GetDeletionTimestamp()) - managed.SetConditions(xpv1.Deleting()) // Empty ConnectionDetails are passed to UnpublishConnection because we // have not retrieved them from the external resource. In practice we @@ -550,7 +680,7 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc // backoff. log.Debug("Cannot unpublish connection details", "error", err) record.Event(managed, event.Warning(reasonCannotUnpublish, err)) - managed.SetConditions(xpv1.ReconcileError(err)) + managed.SetConditions(xpv1.Deleting(), xpv1.ReconcileError(err)) return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) } if err := r.managed.RemoveFinalizer(ctx, managed); err != nil { @@ -559,7 +689,7 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc // condition. If not, we requeue explicitly, which will trigger // backoff. log.Debug("Cannot remove managed resource finalizer", "error", err) - managed.SetConditions(xpv1.ReconcileError(err)) + managed.SetConditions(xpv1.Deleting(), xpv1.ReconcileError(err)) return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) } @@ -581,6 +711,17 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) } + // If we started but never completed creation of an external resource we + // may have lost critical information. For example if we didn't persist + // an updated external name we've leaked a resource. The safest thing to + // do is to refuse to proceed. + if meta.ExternalCreateIncomplete(managed) { + log.Debug(errCreateIncomplete) + record.Event(managed, event.Warning(reasonCannotInitialize, errors.New(errCreateIncomplete))) + managed.SetConditions(xpv1.Creating(), xpv1.ReconcileError(errors.New(errCreateIncomplete))) + return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) + } + // We resolve any references before observing our external resource because // in some rare examples we need a spec field to make the observe call, and // that spec field could be set by a reference. @@ -616,6 +757,12 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc managed.SetConditions(xpv1.ReconcileError(errors.Wrap(err, errReconcileConnect))) return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) } + defer func() { + if err := r.external.Disconnect(ctx); err != nil { + log.Debug("Cannot disconnect from provider", "error", err) + record.Event(managed, event.Warning(reasonCannotDisconnect, err)) + } + }() observation, err := external.Observe(externalCtx, managed) if err != nil { @@ -631,9 +778,19 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) } + // If this resource has a non-zero creation grace period we want to wait + // for that period to expire before we trust that the resource really + // doesn't exist. This is because some external APIs are eventually + // consistent and may report that a recently created resource does not + // exist. + if !observation.ResourceExists && meta.ExternalCreateSucceededDuring(managed, r.creationGracePeriod) { + log.Debug("Waiting for external resource existence to be confirmed") + record.Event(managed, event.Normal(reasonPending, "Waiting for external resource existence to be confirmed")) + return reconcile.Result{Requeue: true}, nil + } + if meta.WasDeleted(managed) { log = log.WithValues("deletion-timestamp", managed.GetDeletionTimestamp()) - managed.SetConditions(xpv1.Deleting()) // We'll only reach this point if deletion policy is not orphan, so we // are safe to call external deletion if external resource exists. @@ -647,7 +804,7 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc // explicitly, which will trigger backoff. log.Debug("Cannot delete external resource", "error", err) record.Event(managed, event.Warning(reasonCannotDelete, err)) - managed.SetConditions(xpv1.ReconcileError(errors.Wrap(err, errReconcileDelete))) + managed.SetConditions(xpv1.Deleting(), xpv1.ReconcileError(errors.Wrap(err, errReconcileDelete))) return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) } @@ -660,7 +817,7 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc // block and try again. log.Debug("Successfully requested deletion of external resource") record.Event(managed, event.Normal(reasonDeleted, "Successfully requested deletion of external resource")) - managed.SetConditions(xpv1.ReconcileSuccess()) + managed.SetConditions(xpv1.Deleting(), xpv1.ReconcileSuccess()) return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) } if err := r.managed.UnpublishConnection(ctx, managed, observation.ConnectionDetails); err != nil { @@ -670,7 +827,7 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc // backoff. log.Debug("Cannot unpublish connection details", "error", err) record.Event(managed, event.Warning(reasonCannotUnpublish, err)) - managed.SetConditions(xpv1.ReconcileError(err)) + managed.SetConditions(xpv1.Deleting(), xpv1.ReconcileError(err)) return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) } if err := r.managed.RemoveFinalizer(ctx, managed); err != nil { @@ -679,7 +836,7 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc // condition. If not, we requeue explicitly, which will trigger // backoff. log.Debug("Cannot remove managed resource finalizer", "error", err) - managed.SetConditions(xpv1.ReconcileError(err)) + managed.SetConditions(xpv1.Deleting(), xpv1.ReconcileError(err)) return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) } @@ -691,7 +848,7 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc return reconcile.Result{Requeue: false}, nil } - if err := r.managed.PublishConnection(ctx, managed, observation.ConnectionDetails); err != nil { + if _, err := r.managed.PublishConnection(ctx, managed, observation.ConnectionDetails); err != nil { // If this is the first time we encounter this issue we'll be requeued // implicitly when we update our status with the new error condition. If // not, we requeue explicitly, which will trigger backoff. @@ -711,7 +868,21 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc } if !observation.ResourceExists { - managed.SetConditions(xpv1.Creating()) + // We write this annotation for two reasons. Firstly, it helps + // us to detect the case in which we fail to persist critical + // information (like the external name) that may be set by the + // subsequent external.Create call. Secondly, it guarantees that + // we're operating on the latest version of our resource. We + // don't use the CriticalAnnotationUpdater because we _want_ the + // update to fail if we get a 409 due to a stale version. + meta.SetExternalCreatePending(managed, time.Now()) + if err := r.client.Update(ctx, managed); err != nil { + log.Debug(errUpdateManaged, "error", err) + record.Event(managed, event.Warning(reasonCannotUpdateManaged, errors.Wrap(err, errUpdateManaged))) + managed.SetConditions(xpv1.Creating(), xpv1.ReconcileError(errors.Wrap(err, errUpdateManaged))) + return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) + } + creation, err := external.Create(externalCtx, managed) if err != nil { // We'll hit this condition if we can't create our external @@ -721,39 +892,58 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc // the new error condition. If not, we requeue explicitly, which will trigger backoff. log.Debug("Cannot create external resource", "error", err) record.Event(managed, event.Warning(reasonCannotCreate, err)) - managed.SetConditions(xpv1.ReconcileError(errors.Wrap(err, errReconcileCreate))) + + // We handle annotations specially here because it's + // critical that they are persisted to the API server. + // If we don't add the external-create-failed annotation + // the reconciler will refuse to proceed, because it + // won't know whether or not it created an external + // resource. + meta.SetExternalCreateFailed(managed, time.Now()) + if err := r.managed.UpdateCriticalAnnotations(ctx, managed); err != nil { + log.Debug(errUpdateManagedAnnotations, "error", err) + record.Event(managed, event.Warning(reasonCannotUpdateManaged, errors.Wrap(err, errUpdateManagedAnnotations))) + + // We only log and emit an event here rather + // than setting a status condition and returning + // early because presumably it's more useful to + // set our status condition to the reason the + // create failed. + } + + managed.SetConditions(xpv1.Creating(), xpv1.ReconcileError(errors.Wrap(err, errReconcileCreate))) return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) } - if creation.ExternalNameAssigned { - en := meta.GetExternalName(managed) - // We will retry in all cases where the error comes from the api-server. - // At one point, context deadline will be exceeded and we'll get out - // of the loop. In that case, we warn the user that the external resource - // might be leaked. - err := retry.OnError(retry.DefaultRetry, resource.IsAPIError, func() error { - nn := types.NamespacedName{Name: managed.GetName()} - if err := r.client.Get(ctx, nn, managed); err != nil { - return err - } - meta.SetExternalName(managed, en) - return r.client.Update(ctx, managed) - }) - if err != nil { - log.Debug("Cannot update managed resource", "error", err) - record.Event(managed, event.Warning(reasonCannotUpdateManaged, errors.Wrap(err, errUpdateManagedAfterCreate))) - managed.SetConditions(xpv1.ReconcileError(errors.Wrap(err, errUpdateManagedAfterCreate))) - return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) - } + // In some cases our external-name may be set by Create above. + log = log.WithValues("external-name", meta.GetExternalName(managed)) + record = r.record.WithAnnotations("external-name", meta.GetExternalName(managed)) + + // We handle annotations specially here because it's critical + // that they are persisted to the API server. If we don't remove + // add the external-create-succeeded annotation the reconciler + // will refuse to proceed, because it won't know whether or not + // it created an external resource. This is also important in + // cases where we must record an external-name annotation set by + // the Create call. Any other changes made during Create will be + // reverted when annotations are updated; at the time of writing + // Create implementations are advised not to alter status, but + // we may revisit this in future. + meta.SetExternalCreateSucceeded(managed, time.Now()) + if err := r.managed.UpdateCriticalAnnotations(ctx, managed); err != nil { + log.Debug(errUpdateManagedAnnotations, "error", err) + record.Event(managed, event.Warning(reasonCannotUpdateManaged, errors.Wrap(err, errUpdateManagedAnnotations))) + managed.SetConditions(xpv1.Creating(), xpv1.ReconcileError(errors.Wrap(err, errUpdateManagedAnnotations))) + return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) } - if err := r.managed.PublishConnection(ctx, managed, creation.ConnectionDetails); err != nil { + if _, err := r.managed.PublishConnection(ctx, managed, creation.ConnectionDetails); err != nil { // If this is the first time we encounter this issue we'll be // requeued implicitly when we update our status with the new error // condition. If not, we requeue explicitly, which will trigger backoff. log.Debug("Cannot publish connection details", "error", err) record.Event(managed, event.Warning(reasonCannotPublish, err)) - managed.SetConditions(xpv1.ReconcileError(err)) + managed.SetConditions(xpv1.Creating(), xpv1.ReconcileError(err)) return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) } @@ -763,7 +953,7 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc // ready for use. log.Debug("Successfully requested creation of external resource") record.Event(managed, event.Normal(reasonCreated, "Successfully requested creation of external resource")) - managed.SetConditions(xpv1.ReconcileSuccess()) + managed.SetConditions(xpv1.Creating(), xpv1.ReconcileSuccess()) return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) } @@ -812,7 +1002,7 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) } - if err := r.managed.PublishConnection(ctx, managed, update.ConnectionDetails); err != nil { + if _, err := r.managed.PublishConnection(ctx, managed, update.ConnectionDetails); err != nil { // If this is the first time we encounter this issue we'll be requeued // implicitly when we update our status with the new error condition. If // not, we requeue explicitly, which will trigger backoff. diff --git a/pkg/reconciler/managed/reconciler_test.go b/pkg/reconciler/managed/reconciler_test.go index 39d02db39..c90011e84 100644 --- a/pkg/reconciler/managed/reconciler_test.go +++ b/pkg/reconciler/managed/reconciler_test.go @@ -19,18 +19,20 @@ package managed import ( "context" "testing" + "time" kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/google/go-cmp/cmp" - "github.com/pkg/errors" + "github.com/google/go-cmp/cmp/cmpopts" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/crossplane/crossplane-runtime/pkg/resource" "github.com/crossplane/crossplane-runtime/pkg/resource/fake" @@ -110,7 +112,7 @@ func TestReconciler(t *testing.T) { mg: resource.ManagedKind(fake.GVK(&fake.Managed{})), o: []ReconcilerOption{ WithConnectionPublishers(ConnectionPublisherFns{ - UnpublishConnectionFn: func(_ context.Context, _ resource.Managed, _ ConnectionDetails) error { return errBoom }, + UnpublishConnectionFn: func(_ context.Context, _ resource.ConnectionSecretOwner, _ ConnectionDetails) error { return errBoom }, }), }, }, @@ -199,6 +201,35 @@ func TestReconciler(t *testing.T) { }, want: want{result: reconcile.Result{Requeue: true}}, }, + "ExternalCreatePending": { + reason: "We should return early if the managed resource appears to be pending creation. We might have leaked a resource and don't want to create another.", + args: args{ + m: &fake.Manager{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + meta.SetExternalCreatePending(obj, now.Time) + return nil + }), + MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error { + want := &fake.Managed{} + meta.SetExternalCreatePending(want, now.Time) + want.SetConditions(xpv1.Creating(), xpv1.ReconcileError(errors.New(errCreateIncomplete))) + if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { + reason := "We should update our status when we're asked to reconcile a managed resource that is pending creation." + t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) + } + return nil + }), + }, + Scheme: fake.SchemeWith(&fake.Managed{}), + }, + mg: resource.ManagedKind(fake.GVK(&fake.Managed{})), + o: []ReconcilerOption{ + WithInitializers(InitializerFn(func(_ context.Context, mg resource.Managed) error { return nil })), + }, + }, + want: want{result: reconcile.Result{Requeue: false}}, + }, "ResolveReferencesError": { reason: "Errors during reference resolution references should trigger a requeue after a short wait.", args: args{ @@ -255,6 +286,45 @@ func TestReconciler(t *testing.T) { }, want: want{result: reconcile.Result{Requeue: true}}, }, + "ExternalDisconnectError": { + reason: "Error disconnecting from the provider should not trigger requeue.", + args: args{ + m: &fake.Manager{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil), + MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error { + want := &fake.Managed{} + want.SetConditions(xpv1.ReconcileSuccess()) + if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { + reason := "A successful no-op reconcile should be reported as a conditioned status." + t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) + } + return nil + }), + }, + Scheme: fake.SchemeWith(&fake.Managed{}), + }, + mg: resource.ManagedKind(fake.GVK(&fake.Managed{})), + o: []ReconcilerOption{ + WithInitializers(), + WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), + WithExternalConnectDisconnecter(ExternalConnectDisconnecterFns{ + ConnectFn: func(_ context.Context, mg resource.Managed) (ExternalClient, error) { + c := &ExternalClientFns{ + ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { + return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil + }, + } + return c, nil + }, + DisconnectFn: func(_ context.Context) error { return errBoom }, + }), + WithConnectionPublishers(), + WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), + }, + }, + want: want{result: reconcile.Result{RequeueAfter: defaultpollInterval}}, + }, "ExternalObserveError": { reason: "Errors observing the external resource should trigger a requeue after a short wait.", args: args{ @@ -288,6 +358,34 @@ func TestReconciler(t *testing.T) { }, want: want{result: reconcile.Result{Requeue: true}}, }, + "CreationGracePeriod": { + reason: "If our resource appears not to exist during the creation grace period we should return early.", + args: args{ + m: &fake.Manager{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + meta.SetExternalCreateSucceeded(obj, time.Now()) + return nil + }), + }, + Scheme: fake.SchemeWith(&fake.Managed{}), + }, + mg: resource.ManagedKind(fake.GVK(&fake.Managed{})), + o: []ReconcilerOption{ + WithInitializers(), + WithCreationGracePeriod(1 * time.Minute), + WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, mg resource.Managed) (ExternalClient, error) { + c := &ExternalClientFns{ + ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { + return ExternalObservation{ResourceExists: false}, nil + }, + } + return c, nil + })), + }, + }, + want: want{result: reconcile.Result{Requeue: true}}, + }, "ExternalDeleteError": { reason: "Errors deleting the external resource should trigger a requeue after a short wait.", args: args{ @@ -417,7 +515,7 @@ func TestReconciler(t *testing.T) { return c, nil })), WithConnectionPublishers(ConnectionPublisherFns{ - UnpublishConnectionFn: func(_ context.Context, _ resource.Managed, _ ConnectionDetails) error { return errBoom }, + UnpublishConnectionFn: func(_ context.Context, _ resource.ConnectionSecretOwner, _ ConnectionDetails) error { return errBoom }, }), }, }, @@ -523,7 +621,9 @@ func TestReconciler(t *testing.T) { WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnecter(&NopConnecter{}), WithConnectionPublishers(ConnectionPublisherFns{ - PublishConnectionFn: func(_ context.Context, _ resource.Managed, _ ConnectionDetails) error { return errBoom }, + PublishConnectionFn: func(_ context.Context, _ resource.ConnectionSecretOwner, _ ConnectionDetails) (bool, error) { + return false, errBoom + }, }), }, }, @@ -558,17 +658,18 @@ func TestReconciler(t *testing.T) { }, want: want{result: reconcile.Result{Requeue: true}}, }, - "CreateExternalError": { - reason: "Errors while creating an external resource should trigger a requeue after a short wait.", + "UpdateCreatePendingError": { + reason: "Errors while updating our external-create-pending annotation should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ - MockGet: test.NewMockGetFn(nil), + MockGet: test.NewMockGetFn(nil), + MockUpdate: test.NewMockUpdateFn(errBoom), MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error { want := &fake.Managed{} - want.SetConditions(xpv1.ReconcileError(errors.Wrap(errBoom, errReconcileCreate))) - want.SetConditions(xpv1.Creating()) - if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { + meta.SetExternalCreatePending(want, time.Now()) + want.SetConditions(xpv1.Creating(), xpv1.ReconcileError(errors.Wrap(errBoom, errUpdateManaged))) + if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Errors while creating an external resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } @@ -598,18 +699,21 @@ func TestReconciler(t *testing.T) { }, want: want{result: reconcile.Result{Requeue: true}}, }, - "PublishCreationConnectionDetailsError": { - reason: "Errors publishing connection details after creation should trigger a requeue after a short wait.", + "CreateExternalError": { + reason: "Errors while creating an external resource should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ - MockGet: test.NewMockGetFn(nil), + MockGet: test.NewMockGetFn(nil), + MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error { want := &fake.Managed{} - want.SetConditions(xpv1.ReconcileError(errBoom)) + meta.SetExternalCreatePending(want, time.Now()) + meta.SetExternalCreateFailed(want, time.Now()) + want.SetConditions(xpv1.ReconcileError(errors.Wrap(errBoom, errReconcileCreate))) want.SetConditions(xpv1.Creating()) - if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { - reason := "Errors publishing connection details after creation should be reported as a conditioned status." + if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { + reason := "Errors while creating an external resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil @@ -627,60 +731,22 @@ func TestReconciler(t *testing.T) { return ExternalObservation{ResourceExists: false}, nil }, CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { - cd := ConnectionDetails{"create": []byte{}} - return ExternalCreation{ConnectionDetails: cd}, nil + return ExternalCreation{}, errBoom }, } return c, nil })), - WithConnectionPublishers(ConnectionPublisherFns{ - PublishConnectionFn: func(_ context.Context, _ resource.Managed, cd ConnectionDetails) error { - // We're called after observe, create, and update - // but we only want to fail when publishing details - // after a creation. - if _, ok := cd["create"]; ok { - return errBoom - } - return nil - }, - }), - WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), - }, - }, - want: want{result: reconcile.Result{Requeue: true}}, - }, - "CreateSuccessful": { - reason: "Successful managed resource creation should trigger a requeue after a short wait.", - args: args{ - m: &fake.Manager{ - Client: &test.MockClient{ - MockGet: test.NewMockGetFn(nil), - MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error { - want := &fake.Managed{} - want.SetConditions(xpv1.ReconcileSuccess()) - want.SetConditions(xpv1.Creating()) - if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { - reason := "Successful managed resource creation should be reported as a conditioned status." - t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) - } - return nil - }), - }, - Scheme: fake.SchemeWith(&fake.Managed{}), - }, - mg: resource.ManagedKind(fake.GVK(&fake.Managed{})), - o: []ReconcilerOption{ - WithInitializers(), - WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), - WithExternalConnecter(&NopConnecter{}), + // We simulate our critical annotation update failing too here. + // This is mostly just to exercise the code, which just creates a log and an event. + WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(ctx context.Context, o client.Object) error { return errBoom })), WithConnectionPublishers(), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, - "CreateWithExternalNameAssignmentSuccessful": { - reason: "Successful managed resource creation with external name assignment should trigger an update.", + "UpdateCriticalAnnotationsError": { + reason: "Errors updating critical annotations after creation should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ @@ -688,11 +754,12 @@ func TestReconciler(t *testing.T) { MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error { want := &fake.Managed{} - meta.SetExternalName(want, "test") - want.SetConditions(xpv1.ReconcileSuccess()) + meta.SetExternalCreatePending(want, time.Now()) + meta.SetExternalCreateSucceeded(want, time.Now()) + want.SetConditions(xpv1.ReconcileError(errors.Wrap(errBoom, errUpdateManagedAnnotations))) want.SetConditions(xpv1.Creating()) - if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { - reason := "Successful managed resource creation should be reported as a conditioned status." + if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { + reason := "Errors updating critical annotations after creation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil @@ -706,40 +773,36 @@ func TestReconciler(t *testing.T) { WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, mg resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ - CreateFn: func(_ context.Context, mg resource.Managed) (ExternalCreation, error) { - meta.SetExternalName(mg, "test") - return ExternalCreation{ExternalNameAssigned: true}, nil - }, ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { - return ExternalObservation{}, nil + return ExternalObservation{ResourceExists: false}, nil + }, + CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { + return ExternalCreation{}, nil }, } return c, nil })), - WithConnectionPublishers(), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), + WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(ctx context.Context, o client.Object) error { return errBoom })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, - "CreateWithExternalNameAssignmentGetError": { - reason: "If the Get call during the update after Create does not go through, we need to inform the user and requeue shortly.", + "PublishCreationConnectionDetailsError": { + reason: "Errors publishing connection details after creation should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ - MockGet: func(_ context.Context, _ client.ObjectKey, obj client.Object) error { - if meta.GetExternalName(obj.(metav1.Object)) == "test" { - return errBoom - } - return nil - }, + MockGet: test.NewMockGetFn(nil), + MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error { want := &fake.Managed{} - meta.SetExternalName(want, "test") - want.SetConditions(xpv1.ReconcileError(errors.Wrap(errBoom, errUpdateManagedAfterCreate))) + meta.SetExternalCreatePending(want, time.Now()) + meta.SetExternalCreateSucceeded(want, time.Now()) + want.SetConditions(xpv1.ReconcileError(errBoom)) want.SetConditions(xpv1.Creating()) - if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { - reason := "Successful managed resource creation should be reported as a conditioned status." + if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { + reason := "Errors publishing connection details after creation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil @@ -753,35 +816,47 @@ func TestReconciler(t *testing.T) { WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, mg resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ - CreateFn: func(_ context.Context, mg resource.Managed) (ExternalCreation, error) { - meta.SetExternalName(mg, "test") - return ExternalCreation{ExternalNameAssigned: true}, nil - }, ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { - return ExternalObservation{}, nil + return ExternalObservation{ResourceExists: false}, nil + }, + CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { + cd := ConnectionDetails{"create": []byte{}} + return ExternalCreation{ConnectionDetails: cd}, nil }, } return c, nil })), - WithConnectionPublishers(), + WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(ctx context.Context, o client.Object) error { return nil })), + WithConnectionPublishers(ConnectionPublisherFns{ + PublishConnectionFn: func(_ context.Context, _ resource.ConnectionSecretOwner, cd ConnectionDetails) (bool, error) { + // We're called after observe, create, and update + // but we only want to fail when publishing details + // after a creation. + if _, ok := cd["create"]; ok { + return false, errBoom + } + return true, nil + }, + }), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, - "CreateWithExternalNameAssignmentUpdateError": { - reason: "If the update after Create does not go through, we need to inform the user and requeue shortly.", + "CreateSuccessful": { + reason: "Successful managed resource creation should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil), - MockUpdate: test.NewMockUpdateFn(errBoom), + MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error { want := &fake.Managed{} - meta.SetExternalName(want, "test") - want.SetConditions(xpv1.ReconcileError(errors.Wrap(errBoom, errUpdateManagedAfterCreate))) + meta.SetExternalCreatePending(want, time.Now()) + meta.SetExternalCreateSucceeded(want, time.Now()) + want.SetConditions(xpv1.ReconcileSuccess()) want.SetConditions(xpv1.Creating()) - if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { + if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Successful managed resource creation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } @@ -794,18 +869,8 @@ func TestReconciler(t *testing.T) { o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), - WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, mg resource.Managed) (ExternalClient, error) { - c := &ExternalClientFns{ - CreateFn: func(_ context.Context, mg resource.Managed) (ExternalCreation, error) { - meta.SetExternalName(mg, "test") - return ExternalCreation{ExternalNameAssigned: true}, nil - }, - ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { - return ExternalObservation{}, nil - }, - } - return c, nil - })), + WithExternalConnecter(&NopConnecter{}), + WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(ctx context.Context, o client.Object) error { return nil })), WithConnectionPublishers(), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, @@ -959,14 +1024,14 @@ func TestReconciler(t *testing.T) { return c, nil })), WithConnectionPublishers(ConnectionPublisherFns{ - PublishConnectionFn: func(_ context.Context, _ resource.Managed, cd ConnectionDetails) error { + PublishConnectionFn: func(_ context.Context, _ resource.ConnectionSecretOwner, cd ConnectionDetails) (bool, error) { // We're called after observe, create, and update // but we only want to fail when publishing details // after an update. if _, ok := cd["update"]; ok { - return errBoom + return false, errBoom } - return nil + return false, nil }, }), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), diff --git a/pkg/reconciler/providerconfig/reconciler.go b/pkg/reconciler/providerconfig/reconciler.go index 58568e99d..107b4d9b3 100644 --- a/pkg/reconciler/providerconfig/reconciler.go +++ b/pkg/reconciler/providerconfig/reconciler.go @@ -21,7 +21,6 @@ import ( "strings" "time" - "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" @@ -29,6 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/event" "github.com/crossplane/crossplane-runtime/pkg/logging" "github.com/crossplane/crossplane-runtime/pkg/meta" @@ -138,11 +138,11 @@ func NewReconciler(m manager.Manager, of resource.ProviderConfigKinds, o ...Reco // Reconcile a ProviderConfig by accounting for the managed resources that are // using it, and ensuring it cannot be deleted until it is no longer in use. -func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconcile.Result, error) { +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { log := r.log.WithValues("request", req) log.Debug("Reconciling") - ctx, cancel := context.WithTimeout(context.Background(), timeout) + ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() pc := r.newConfig() diff --git a/pkg/reconciler/providerconfig/reconciler_test.go b/pkg/reconciler/providerconfig/reconciler_test.go index 8991075b5..2bdc0a71a 100644 --- a/pkg/reconciler/providerconfig/reconciler_test.go +++ b/pkg/reconciler/providerconfig/reconciler_test.go @@ -22,7 +22,6 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/pkg/errors" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -32,6 +31,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/resource" "github.com/crossplane/crossplane-runtime/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/pkg/test" diff --git a/pkg/reference/reference.go b/pkg/reference/reference.go index 82dc8569b..971c728c6 100644 --- a/pkg/reference/reference.go +++ b/pkg/reference/reference.go @@ -19,11 +19,13 @@ package reference import ( "context" - "github.com/pkg/errors" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/crossplane/crossplane-runtime/pkg/resource" ) @@ -112,11 +114,24 @@ type ResolutionRequest struct { // IsNoOp returns true if the supplied ResolutionRequest cannot or should not be // processed. -func (rr ResolutionRequest) IsNoOp() bool { - // We don't resolve values that are already set; we effectively cache - // resolved values. The CR author can invalidate the cache and trigger a new - // resolution by explicitly clearing the resolved value. - if rr.CurrentValue != "" { +func (rr *ResolutionRequest) IsNoOp() bool { + isAlways := false + if rr.Selector != nil { + if rr.Selector.Policy.IsResolvePolicyAlways() { + rr.Reference = nil + isAlways = true + } + } else if rr.Reference != nil { + if rr.Reference.Policy.IsResolvePolicyAlways() { + isAlways = true + } + } + + // We don't resolve values that are already set (if reference resolution policy + // is not set to Always); we effectively cache resolved values. The CR author + // can invalidate the cache and trigger a new resolution by explicitly clearing + // the resolved value. + if rr.CurrentValue != "" && !isAlways { return true } @@ -153,13 +168,28 @@ type MultiResolutionRequest struct { // IsNoOp returns true if the supplied MultiResolutionRequest cannot or should // not be processed. -func (rr MultiResolutionRequest) IsNoOp() bool { - // We don't resolve values that are already set; we effectively cache - // resolved values. The CR author can invalidate the cache and trigger a new - // resolution by explicitly clearing the resolved values. This is a little - // unintuitive for the APIMultiResolver but mimics the UX of the APIResolver - // and simplifies the overall mental model. - if len(rr.CurrentValues) > 0 { +func (rr *MultiResolutionRequest) IsNoOp() bool { + isAlways := false + if rr.Selector != nil { + if rr.Selector.Policy.IsResolvePolicyAlways() { + rr.References = nil + isAlways = true + } + } else { + for _, r := range rr.References { + if r.Policy.IsResolvePolicyAlways() { + isAlways = true + break + } + } + } + + // We don't resolve values that are already set (if reference resolution policy + // is not set to Always); we effectively cache resolved values. The CR author + // can invalidate the cache and trigger a new resolution by explicitly clearing + // the resolved values. This is a little unintuitive for the APIMultiResolver + // but mimics the UX of the APIResolver and simplifies the overall mental model. + if len(rr.CurrentValues) > 0 && !isAlways { return true } @@ -182,9 +212,9 @@ func (rr MultiResolutionResponse) Validate() error { return errors.New(errNoMatches) } - for _, v := range rr.ResolvedValues { + for i, v := range rr.ResolvedValues { if v == "" { - return errors.New(errNoValue) + return getResolutionError(rr.ResolvedReferences[i].Policy, errors.New(errNoValue)) } } @@ -216,11 +246,14 @@ func (r *APIResolver) Resolve(ctx context.Context, req ResolutionRequest) (Resol // The reference is already set - resolve it. if req.Reference != nil { if err := r.client.Get(ctx, types.NamespacedName{Name: req.Reference.Name}, req.To.Managed); err != nil { + if kerrors.IsNotFound(err) { + return ResolutionResponse{}, getResolutionError(req.Reference.Policy, errors.Wrap(err, errGetManaged)) + } return ResolutionResponse{}, errors.Wrap(err, errGetManaged) } rsp := ResolutionResponse{ResolvedValue: req.Extract(req.To.Managed), ResolvedReference: req.Reference} - return rsp, rsp.Validate() + return rsp, getResolutionError(req.Reference.Policy, rsp.Validate()) } // The reference was not set, but a selector was. Select a reference. @@ -234,17 +267,18 @@ func (r *APIResolver) Resolve(ctx context.Context, req ResolutionRequest) (Resol } rsp := ResolutionResponse{ResolvedValue: req.Extract(to), ResolvedReference: &xpv1.Reference{Name: to.GetName()}} - return rsp, rsp.Validate() + return rsp, getResolutionError(req.Selector.Policy, rsp.Validate()) } // We couldn't resolve anything. - return ResolutionResponse{}, errors.New(errNoMatches) + return ResolutionResponse{}, getResolutionError(req.Selector.Policy, errors.New(errNoMatches)) + } // ResolveMultiple resolves the supplied MultiResolutionRequest. The returned // MultiResolutionResponse always contains valid values unless an error was // returned. -func (r *APIResolver) ResolveMultiple(ctx context.Context, req MultiResolutionRequest) (MultiResolutionResponse, error) { +func (r *APIResolver) ResolveMultiple(ctx context.Context, req MultiResolutionRequest) (MultiResolutionResponse, error) { // nolint: gocyclo // Return early if from is being deleted, or the request is a no-op. if meta.WasDeleted(r.from) || req.IsNoOp() { return MultiResolutionResponse{ResolvedValues: req.CurrentValues, ResolvedReferences: req.References}, nil @@ -255,6 +289,9 @@ func (r *APIResolver) ResolveMultiple(ctx context.Context, req MultiResolutionRe vals := make([]string, len(req.References)) for i := range req.References { if err := r.client.Get(ctx, types.NamespacedName{Name: req.References[i].Name}, req.To.Managed); err != nil { + if kerrors.IsNotFound(err) { + return MultiResolutionResponse{}, getResolutionError(req.References[i].Policy, errors.Wrap(err, errGetManaged)) + } return MultiResolutionResponse{}, errors.Wrap(err, errGetManaged) } vals[i] = req.Extract(req.To.Managed) @@ -282,7 +319,14 @@ func (r *APIResolver) ResolveMultiple(ctx context.Context, req MultiResolutionRe } rsp := MultiResolutionResponse{ResolvedValues: vals, ResolvedReferences: refs} - return rsp, rsp.Validate() + return rsp, getResolutionError(req.Selector.Policy, rsp.Validate()) +} + +func getResolutionError(p *xpv1.Policy, err error) error { + if !p.IsResolutionPolicyOptional() { + return err + } + return nil } // ControllersMustMatch returns true if the supplied Selector requires that a diff --git a/pkg/reference/reference_test.go b/pkg/reference/reference_test.go index ca774e7a8..27ba36189 100644 --- a/pkg/reference/reference_test.go +++ b/pkg/reference/reference_test.go @@ -22,12 +22,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/crossplane/crossplane-runtime/pkg/resource" "github.com/crossplane/crossplane-runtime/pkg/resource/fake" @@ -91,6 +91,10 @@ func TestResolve(t *testing.T) { now := metav1.Now() value := "coolv" ref := &xpv1.Reference{Name: "cool"} + optionalPolicy := xpv1.ResolutionPolicyOptional + alwaysPolicy := xpv1.ResolvePolicyAlways + optionalRef := &xpv1.Reference{Name: "cool", Policy: &xpv1.Policy{Resolution: &optionalPolicy}} + alwaysRef := &xpv1.Reference{Name: "cool", Policy: &xpv1.Policy{Resolve: &alwaysPolicy}} controlled := &fake.Managed{} controlled.SetName(value) @@ -134,6 +138,32 @@ func TestResolve(t *testing.T) { err: nil, }, }, + "AlwaysResolveReference": { + reason: "Should not return early if the current value is non-zero, when the resolve policy is set to" + + "Always", + c: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + meta.SetExternalName(obj.(metav1.Object), value) + return nil + }), + }, + from: &fake.Managed{}, + args: args{ + req: ResolutionRequest{ + Reference: alwaysRef, + To: To{Managed: &fake.Managed{}}, + Extract: ExternalName(), + CurrentValue: "oldValue", + }, + }, + want: want{ + rsp: ResolutionResponse{ + ResolvedValue: value, + ResolvedReference: alwaysRef, + }, + err: nil, + }, + }, "Unresolvable": { reason: "Should return early if neither a reference or selector were provided", from: &fake.Managed{}, @@ -204,6 +234,26 @@ func TestResolve(t *testing.T) { }, }, }, + "OptionalReference": { + reason: "No error should be returned when the resolution policy is Optional", + c: &test.MockClient{ + MockGet: test.NewMockGetFn(nil), + }, + from: &fake.Managed{}, + args: args{ + req: ResolutionRequest{ + Reference: optionalRef, + To: To{Managed: &fake.Managed{}}, + Extract: func(resource.Managed) string { return "" }, + }, + }, + want: want{ + rsp: ResolutionResponse{ + ResolvedReference: optionalRef, + }, + err: nil, + }, + }, "ListError": { reason: "Should return errors encountered while listing potential referenced resources", c: &test.MockClient{ @@ -237,6 +287,25 @@ func TestResolve(t *testing.T) { err: errors.New(errNoMatches), }, }, + "OptionalSelector": { + reason: "No error should be returned when the resolution policy is Optional", + c: &test.MockClient{ + MockList: test.NewMockListFn(nil), + }, + from: &fake.Managed{}, + args: args{ + req: ResolutionRequest{ + Selector: &xpv1.Selector{ + Policy: &xpv1.Policy{Resolution: &optionalPolicy}, + }, + To: To{List: &FakeManagedList{}}, + }, + }, + want: want{ + rsp: ResolutionResponse{}, + err: nil, + }, + }, "SuccessfulSelect": { reason: "A managed resource with a matching controller reference should be selected and returned", c: &test.MockClient{ @@ -263,6 +332,61 @@ func TestResolve(t *testing.T) { err: nil, }, }, + "AlwaysResolveSelector": { + reason: "Should not return early if the current value is non-zero, when the resolve policy is set to" + + "Always", + c: &test.MockClient{ + MockList: test.NewMockListFn(nil), + }, + from: controlled, + args: args{ + req: ResolutionRequest{ + Selector: &xpv1.Selector{ + MatchControllerRef: func() *bool { t := true; return &t }(), + Policy: &xpv1.Policy{Resolve: &alwaysPolicy}, + }, + To: To{List: &FakeManagedList{Items: []resource.Managed{ + &fake.Managed{}, // A resource that does not match. + controlled, // A resource with a matching controller reference. + }}}, + Extract: ExternalName(), + CurrentValue: "oldValue", + }, + }, + want: want{ + rsp: ResolutionResponse{ + ResolvedValue: value, + ResolvedReference: &xpv1.Reference{Name: value}, + }, + err: nil, + }, + }, + "BothReferenceSelector": { + reason: "When both Reference and Selector fields set and Policy is not set, the Reference must be resolved", + c: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + meta.SetExternalName(obj.(metav1.Object), value) + return nil + }), + }, + from: &fake.Managed{}, + args: args{ + req: ResolutionRequest{ + Reference: ref, + Selector: &xpv1.Selector{ + MatchControllerRef: func() *bool { t := true; return &t }(), + }, + To: To{Managed: &fake.Managed{}}, + Extract: ExternalName(), + }, + }, + want: want{ + rsp: ResolutionResponse{ + ResolvedValue: value, + ResolvedReference: ref, + }, + }, + }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { @@ -282,6 +406,10 @@ func TestResolveMultiple(t *testing.T) { now := metav1.Now() value := "coolv" ref := xpv1.Reference{Name: "cool"} + optionalPolicy := xpv1.ResolutionPolicyOptional + alwaysPolicy := xpv1.ResolvePolicyAlways + optionalRef := xpv1.Reference{Name: "cool", Policy: &xpv1.Policy{Resolution: &optionalPolicy}} + alwaysRef := xpv1.Reference{Name: "cool", Policy: &xpv1.Policy{Resolve: &alwaysPolicy}} controlled := &fake.Managed{} controlled.SetName(value) @@ -325,6 +453,32 @@ func TestResolveMultiple(t *testing.T) { err: nil, }, }, + "AlwaysResolveReference": { + reason: "Should not return early if the current value is non-zero, when the resolve policy is set to" + + "Always", + c: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + meta.SetExternalName(obj.(metav1.Object), value) + return nil + }), + }, + from: &fake.Managed{}, + args: args{ + req: MultiResolutionRequest{ + References: []xpv1.Reference{alwaysRef}, + To: To{Managed: &fake.Managed{}}, + Extract: ExternalName(), + CurrentValues: []string{"oldValue"}, + }, + }, + want: want{ + rsp: MultiResolutionResponse{ + ResolvedValues: []string{value}, + ResolvedReferences: []xpv1.Reference{alwaysRef}, + }, + err: nil, + }, + }, "Unresolvable": { reason: "Should return early if neither a reference or selector were provided", from: &fake.Managed{}, @@ -396,6 +550,27 @@ func TestResolveMultiple(t *testing.T) { }, }, }, + "OptionalReference": { + reason: "No error should be returned when the resolution policy is Optional", + c: &test.MockClient{ + MockGet: test.NewMockGetFn(nil), + }, + from: &fake.Managed{}, + args: args{ + req: MultiResolutionRequest{ + References: []xpv1.Reference{optionalRef}, + To: To{Managed: &fake.Managed{}}, + Extract: func(resource.Managed) string { return "" }, + }, + }, + want: want{ + rsp: MultiResolutionResponse{ + ResolvedValues: []string{""}, + ResolvedReferences: []xpv1.Reference{optionalRef}, + }, + err: nil, + }, + }, "ListError": { reason: "Should return errors encountered while listing potential referenced resources", c: &test.MockClient{ @@ -429,6 +604,25 @@ func TestResolveMultiple(t *testing.T) { err: errors.New(errNoMatches), }, }, + "OptionalSelector": { + reason: "No error should be returned when the resolution policy is Optional", + c: &test.MockClient{ + MockList: test.NewMockListFn(nil), + }, + from: &fake.Managed{}, + args: args{ + req: MultiResolutionRequest{ + Selector: &xpv1.Selector{ + Policy: &xpv1.Policy{Resolution: &optionalPolicy}, + }, + To: To{List: &FakeManagedList{}}, + }, + }, + want: want{ + rsp: MultiResolutionResponse{}, + err: nil, + }, + }, "SuccessfulSelect": { reason: "A managed resource with a matching controller reference should be selected and returned", c: &test.MockClient{ @@ -455,6 +649,61 @@ func TestResolveMultiple(t *testing.T) { err: nil, }, }, + "AlwaysResolveSelector": { + reason: "Should not return early if the current value is non-zero, when the resolve policy is set to" + + "Always", + c: &test.MockClient{ + MockList: test.NewMockListFn(nil), + }, + from: controlled, + args: args{ + req: MultiResolutionRequest{ + Selector: &xpv1.Selector{ + MatchControllerRef: func() *bool { t := true; return &t }(), + Policy: &xpv1.Policy{Resolve: &alwaysPolicy}, + }, + To: To{List: &FakeManagedList{Items: []resource.Managed{ + &fake.Managed{}, // A resource that does not match. + controlled, // A resource with a matching controller reference. + }}}, + Extract: ExternalName(), + CurrentValues: []string{"oldValue"}, + }, + }, + want: want{ + rsp: MultiResolutionResponse{ + ResolvedValues: []string{value}, + ResolvedReferences: []xpv1.Reference{{Name: value}}, + }, + err: nil, + }, + }, + "BothReferenceSelector": { + reason: "When both Reference and Selector fields set and Policy is not set, the Reference must be resolved", + c: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + meta.SetExternalName(obj.(metav1.Object), value) + return nil + }), + }, + from: &fake.Managed{}, + args: args{ + req: MultiResolutionRequest{ + References: []xpv1.Reference{ref}, + Selector: &xpv1.Selector{ + MatchControllerRef: func() *bool { t := true; return &t }(), + }, + To: To{Managed: &fake.Managed{}}, + Extract: ExternalName(), + }, + }, + want: want{ + rsp: MultiResolutionResponse{ + ResolvedValues: []string{value}, + ResolvedReferences: []xpv1.Reference{ref}, + }, + }, + }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { diff --git a/pkg/resource/api.go b/pkg/resource/api.go index 4ad3430ed..5ec906242 100644 --- a/pkg/resource/api.go +++ b/pkg/resource/api.go @@ -20,7 +20,6 @@ import ( "context" "encoding/json" - "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -28,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/meta" ) @@ -210,6 +210,18 @@ type APIFinalizer struct { finalizer string } +// NewNopFinalizer returns a Finalizer that does nothing. +func NewNopFinalizer() Finalizer { return nopFinalizer{} } + +type nopFinalizer struct{} + +func (f nopFinalizer) AddFinalizer(ctx context.Context, obj Object) error { + return nil +} +func (f nopFinalizer) RemoveFinalizer(ctx context.Context, obj Object) error { + return nil +} + // NewAPIFinalizer returns a new APIFinalizer. func NewAPIFinalizer(c client.Client, finalizer string) *APIFinalizer { return &APIFinalizer{client: c, finalizer: finalizer} diff --git a/pkg/resource/api_test.go b/pkg/resource/api_test.go index faa0e98c0..f8b52e33c 100644 --- a/pkg/resource/api_test.go +++ b/pkg/resource/api_test.go @@ -21,7 +21,6 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -30,6 +29,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/crossplane/crossplane-runtime/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/pkg/test" diff --git a/pkg/resource/fake/mocks.go b/pkg/resource/fake/mocks.go index 10b653196..5cbe1d29c 100644 --- a/pkg/resource/fake/mocks.go +++ b/pkg/resource/fake/mocks.go @@ -136,6 +136,21 @@ func (m *ConnectionSecretWriterTo) GetWriteConnectionSecretToReference() *xpv1.S return m.Ref } +// ConnectionDetailsPublisherTo is a mock that implements ConnectionDetailsPublisherTo interface. +type ConnectionDetailsPublisherTo struct { + To *xpv1.PublishConnectionDetailsTo +} + +// SetPublishConnectionDetailsTo sets the PublishConnectionDetailsTo. +func (m *ConnectionDetailsPublisherTo) SetPublishConnectionDetailsTo(to *xpv1.PublishConnectionDetailsTo) { + m.To = to +} + +// GetPublishConnectionDetailsTo gets the PublishConnectionDetailsTo. +func (m *ConnectionDetailsPublisherTo) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { + return m.To +} + // Orphanable implements the Orphanable interface. type Orphanable struct{ Policy xpv1.DeletionPolicy } @@ -263,6 +278,7 @@ type Managed struct { ProviderReferencer ProviderConfigReferencer ConnectionSecretWriterTo + ConnectionDetailsPublisherTo Orphanable xpv1.ConditionedStatus } @@ -293,6 +309,7 @@ type Composite struct { ComposedResourcesReferencer ClaimReferencer ConnectionSecretWriterTo + ConnectionDetailsPublisherTo xpv1.ConditionedStatus ConnectionDetailsLastPublishedTimer @@ -318,6 +335,7 @@ func (m *Composite) DeepCopyObject() runtime.Object { type Composed struct { metav1.ObjectMeta ConnectionSecretWriterTo + ConnectionDetailsPublisherTo xpv1.ConditionedStatus } @@ -346,6 +364,7 @@ type CompositeClaim struct { CompositionUpdater CompositeResourceReferencer LocalConnectionSecretWriterTo + ConnectionDetailsPublisherTo xpv1.ConditionedStatus ConnectionDetailsLastPublishedTimer @@ -417,17 +436,28 @@ type MockConnectionSecretOwner struct { runtime.Object metav1.ObjectMeta - Ref *xpv1.SecretReference + To *xpv1.PublishConnectionDetailsTo + WriterTo *xpv1.SecretReference +} + +// GetPublishConnectionDetailsTo returns the publish connection details to reference. +func (m *MockConnectionSecretOwner) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { + return m.To +} + +// SetPublishConnectionDetailsTo sets the publish connection details to reference. +func (m *MockConnectionSecretOwner) SetPublishConnectionDetailsTo(t *xpv1.PublishConnectionDetailsTo) { + m.To = t } // GetWriteConnectionSecretToReference returns the connection secret reference. func (m *MockConnectionSecretOwner) GetWriteConnectionSecretToReference() *xpv1.SecretReference { - return m.Ref + return m.WriterTo } // SetWriteConnectionSecretToReference sets the connection secret reference. func (m *MockConnectionSecretOwner) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { - m.Ref = r + m.WriterTo = r } // GetObjectKind returns schema.ObjectKind. @@ -453,6 +483,7 @@ type MockLocalConnectionSecretOwner struct { metav1.ObjectMeta Ref *xpv1.LocalSecretReference + To *xpv1.PublishConnectionDetailsTo } // GetWriteConnectionSecretToReference returns the connection secret reference. @@ -465,6 +496,16 @@ func (m *MockLocalConnectionSecretOwner) SetWriteConnectionSecretToReference(r * m.Ref = r } +// SetPublishConnectionDetailsTo sets the publish connectionDetails to +func (m *MockLocalConnectionSecretOwner) SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo) { + m.To = r +} + +// GetPublishConnectionDetailsTo returns the publish connectionDetails to. +func (m *MockLocalConnectionSecretOwner) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { + return m.To +} + // GetObjectKind returns schema.ObjectKind. func (m *MockLocalConnectionSecretOwner) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind diff --git a/pkg/resource/interfaces.go b/pkg/resource/interfaces.go index 049e0e22b..fa3cd1d9e 100644 --- a/pkg/resource/interfaces.go +++ b/pkg/resource/interfaces.go @@ -60,6 +60,13 @@ type ConnectionSecretWriterTo interface { GetWriteConnectionSecretToReference() *xpv1.SecretReference } +// A ConnectionDetailsPublisherTo may write a connection details secret to a +// secret store +type ConnectionDetailsPublisherTo interface { + SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo) + GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo +} + // An Orphanable resource may specify a DeletionPolicy. type Orphanable interface { SetDeletionPolicy(p xpv1.DeletionPolicy) @@ -162,6 +169,7 @@ type Managed interface { ProviderReferencer ProviderConfigReferencer ConnectionSecretWriterTo + ConnectionDetailsPublisherTo Orphanable Conditioned @@ -210,6 +218,7 @@ type Composite interface { ComposedResourcesReferencer ClaimReferencer ConnectionSecretWriterTo + ConnectionDetailsPublisherTo Conditioned ConnectionDetailsPublishedTimer @@ -221,6 +230,7 @@ type Composed interface { Conditioned ConnectionSecretWriterTo + ConnectionDetailsPublisherTo } // A CompositeClaim for a Composite resource. @@ -233,6 +243,7 @@ type CompositeClaim interface { CompositionRevisionReferencer CompositeResourceReferencer LocalConnectionSecretWriterTo + ConnectionDetailsPublisherTo Conditioned ConnectionDetailsPublishedTimer diff --git a/pkg/resource/predicates_test.go b/pkg/resource/predicates_test.go index 415f3eb2b..3001a7750 100644 --- a/pkg/resource/predicates_test.go +++ b/pkg/resource/predicates_test.go @@ -20,12 +20,12 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/crossplane/crossplane-runtime/pkg/resource/fake" ) diff --git a/pkg/resource/providerconfig.go b/pkg/resource/providerconfig.go index 9b6d1dcc4..5da8d78fa 100644 --- a/pkg/resource/providerconfig.go +++ b/pkg/resource/providerconfig.go @@ -20,7 +20,6 @@ import ( "context" "os" - "github.com/pkg/errors" "github.com/spf13/afero" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -29,6 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/meta" ) @@ -95,6 +95,8 @@ func CommonCredentialExtractor(ctx context.Context, source xpv1.CredentialsSourc return ExtractFs(ctx, afero.NewOsFs(), selector) case xpv1.CredentialsSourceSecret: return ExtractSecret(ctx, client, selector) + case xpv1.CredentialsSourceInjectedIdentity: + return nil, nil case xpv1.CredentialsSourceNone: return nil, nil } diff --git a/pkg/resource/providerconfig_test.go b/pkg/resource/providerconfig_test.go index edc1bf1bd..eb973bb6f 100644 --- a/pkg/resource/providerconfig_test.go +++ b/pkg/resource/providerconfig_test.go @@ -21,12 +21,12 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/pkg/errors" "github.com/spf13/afero" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/pkg/test" ) diff --git a/pkg/resource/resource.go b/pkg/resource/resource.go index a0a8d3b42..4b35e1c89 100644 --- a/pkg/resource/resource.go +++ b/pkg/resource/resource.go @@ -20,7 +20,6 @@ import ( "context" "strings" - "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,6 +31,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/meta" ) @@ -63,6 +63,14 @@ type ProviderConfigKinds struct { UsageList schema.GroupVersionKind } +// A ConnectionSecretOwner is a Kubernetes object that owns a connection secret. +type ConnectionSecretOwner interface { + Object + + ConnectionSecretWriterTo + ConnectionDetailsPublisherTo +} + // A LocalConnectionSecretOwner may create and manage a connection secret in its // own namespace. type LocalConnectionSecretOwner interface { @@ -70,6 +78,7 @@ type LocalConnectionSecretOwner interface { metav1.Object LocalConnectionSecretWriterTo + ConnectionDetailsPublisherTo } // A ConnectionPropagator is responsible for propagating information required to @@ -115,15 +124,6 @@ func LocalConnectionSecretFor(o LocalConnectionSecretOwner, kind schema.GroupVer } } -// A ConnectionSecretOwner may create and manage a connection secret in an -// arbitrary namespace. -type ConnectionSecretOwner interface { - runtime.Object - metav1.Object - - ConnectionSecretWriterTo -} - // ConnectionSecretFor creates a connection for the supplied // ConnectionSecretOwner, assumed to be of the supplied kind. The secret is // written to 'default' namespace if the ConnectionSecretOwner does not specify @@ -362,6 +362,11 @@ func (e errNotAllowed) NotAllowed() bool { return true } +// NewNotAllowed returns a new NotAllowed error +func NewNotAllowed(message string) error { + return errNotAllowed{error: errors.New(message)} +} + // IsNotAllowed returns true if the supplied error indicates that an operation // was not allowed. func IsNotAllowed(err error) bool { diff --git a/pkg/resource/resource_test.go b/pkg/resource/resource_test.go index 42be55246..9a734fc30 100644 --- a/pkg/resource/resource_test.go +++ b/pkg/resource/resource_test.go @@ -22,8 +22,6 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -35,6 +33,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/pkg/test" ) @@ -132,7 +131,7 @@ func TestConnectionSecretFor(t *testing.T) { Name: name, UID: uid, }, - Ref: &xpv1.SecretReference{Namespace: namespace, Name: secretName}, + WriterTo: &xpv1.SecretReference{Namespace: namespace, Name: secretName}, }, kind: MockOwnerGVK, }, @@ -822,7 +821,7 @@ func TestApplicatorWithRetry_Apply(t *testing.T) { backoff: tc.fields.backoff, } - if diff := cmp.Diff(tc.wantErr, awr.Apply(tc.args.ctx, tc.args.c, tc.args.opts...), cmpopts.EquateErrors()); diff != "" { + if diff := cmp.Diff(tc.wantErr, awr.Apply(tc.args.ctx, tc.args.c, tc.args.opts...), test.EquateErrors()); diff != "" { t.Fatalf("ApplicatorWithRetry.Apply(...): -want, +got:\n%s", diff) } diff --git a/pkg/resource/unstructured/claim/claim.go b/pkg/resource/unstructured/claim/claim.go index e36bf8e43..fc79babae 100644 --- a/pkg/resource/unstructured/claim/claim.go +++ b/pkg/resource/unstructured/claim/claim.go @@ -150,6 +150,20 @@ func (c *Unstructured) SetWriteConnectionSecretToReference(ref *xpv1.LocalSecret _ = fieldpath.Pave(c.Object).SetValue("spec.writeConnectionSecretToRef", ref) } +// GetPublishConnectionDetailsTo of this composite resource claim. +func (c *Unstructured) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { + out := &xpv1.PublishConnectionDetailsTo{} + if err := fieldpath.Pave(c.Object).GetValueInto("spec.publishConnectionDetailsTo", out); err != nil { + return nil + } + return out +} + +// SetPublishConnectionDetailsTo of this composite resource claim. +func (c *Unstructured) SetPublishConnectionDetailsTo(ref *xpv1.PublishConnectionDetailsTo) { + _ = fieldpath.Pave(c.Object).SetValue("spec.publishConnectionDetailsTo", ref) +} + // GetCondition of this composite resource claim. func (c *Unstructured) GetCondition(ct xpv1.ConditionType) xpv1.Condition { conditioned := xpv1.ConditionedStatus{} diff --git a/pkg/resource/unstructured/composed/composed.go b/pkg/resource/unstructured/composed/composed.go index cf3795da4..5454a0719 100644 --- a/pkg/resource/unstructured/composed/composed.go +++ b/pkg/resource/unstructured/composed/composed.go @@ -98,3 +98,17 @@ func (cr *Unstructured) GetWriteConnectionSecretToReference() *xpv1.SecretRefere func (cr *Unstructured) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { _ = fieldpath.Pave(cr.Object).SetValue("spec.writeConnectionSecretToRef", r) } + +// GetPublishConnectionDetailsTo of this Composed resource. +func (cr *Unstructured) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { + out := &xpv1.PublishConnectionDetailsTo{} + if err := fieldpath.Pave(cr.Object).GetValueInto("spec.publishConnectionDetailsTo", out); err != nil { + return nil + } + return out +} + +// SetPublishConnectionDetailsTo of this Composed resource. +func (cr *Unstructured) SetPublishConnectionDetailsTo(ref *xpv1.PublishConnectionDetailsTo) { + _ = fieldpath.Pave(cr.Object).SetValue("spec.publishConnectionDetailsTo", ref) +} diff --git a/pkg/resource/unstructured/composite/composite.go b/pkg/resource/unstructured/composite/composite.go index 9c95d8c03..efbad6912 100644 --- a/pkg/resource/unstructured/composite/composite.go +++ b/pkg/resource/unstructured/composite/composite.go @@ -172,6 +172,20 @@ func (c *Unstructured) SetWriteConnectionSecretToReference(ref *xpv1.SecretRefer _ = fieldpath.Pave(c.Object).SetValue("spec.writeConnectionSecretToRef", ref) } +// GetPublishConnectionDetailsTo of this Composite resource. +func (c *Unstructured) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { + out := &xpv1.PublishConnectionDetailsTo{} + if err := fieldpath.Pave(c.Object).GetValueInto("spec.publishConnectionDetailsTo", out); err != nil { + return nil + } + return out +} + +// SetPublishConnectionDetailsTo of this Composite resource. +func (c *Unstructured) SetPublishConnectionDetailsTo(ref *xpv1.PublishConnectionDetailsTo) { + _ = fieldpath.Pave(c.Object).SetValue("spec.publishConnectionDetailsTo", ref) +} + // GetCondition of this Composite resource. func (c *Unstructured) GetCondition(ct xpv1.ConditionType) xpv1.Condition { conditioned := xpv1.ConditionedStatus{} diff --git a/pkg/test/cmp.go b/pkg/test/cmp.go index 8717674ab..a7414c1bf 100644 --- a/pkg/test/cmp.go +++ b/pkg/test/cmp.go @@ -25,13 +25,14 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" ) -// TODO(negz): Replace this if a similar option is added to cmpopts per -// https://github.com/google/go-cmp/issues/89 - // EquateErrors returns true if the supplied errors are of the same type and // produce identical strings. This mirrors the error comparison behaviour of // https://github.com/go-test/deep, which most Crossplane tests targeted before // we switched to go-cmp. +// +// This differs from cmpopts.EquateErrors, which does not test for error strings +// and instead returns whether one error 'is' (in the errors.Is sense) the +// other. func EquateErrors() cmp.Option { return cmp.Comparer(func(a, b error) bool { if a == nil || b == nil { diff --git a/pkg/test/fake.go b/pkg/test/fake.go index feb13112b..f11f8cad1 100644 --- a/pkg/test/fake.go +++ b/pkg/test/fake.go @@ -53,6 +53,9 @@ type MockStatusUpdateFn func(ctx context.Context, obj client.Object, opts ...cli // A MockStatusPatchFn is used to mock client.Client's StatusUpdate implementation. type MockStatusPatchFn func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error +// A MockSchemeFn is used to mock client.Client's Scheme implementation. +type MockSchemeFn func() *runtime.Scheme + // An ObjectFn operates on the supplied Object. You might use an ObjectFn to // test or update the contents of an Object. type ObjectFn func(obj client.Object) error @@ -169,6 +172,13 @@ func NewMockStatusPatchFn(err error, ofn ...ObjectFn) MockStatusPatchFn { } } +// NewMockSchemeFn returns a MockSchemeFn that returns the scheme +func NewMockSchemeFn(scheme *runtime.Scheme) MockSchemeFn { + return func() *runtime.Scheme { + return scheme + } +} + // MockClient implements controller-runtime's Client interface, allowing each // method to be overridden for testing. The controller-runtime provides a fake // client, but it is has surprising side effects (e.g. silently calling @@ -183,6 +193,8 @@ type MockClient struct { MockPatch MockPatchFn MockStatusUpdate MockStatusUpdateFn MockStatusPatch MockStatusPatchFn + + MockScheme MockSchemeFn } // NewMockClient returns a MockClient that does nothing when its methods are @@ -198,6 +210,8 @@ func NewMockClient() *MockClient { MockPatch: NewMockPatchFn(nil), MockStatusUpdate: NewMockStatusUpdateFn(nil), MockStatusPatch: NewMockStatusPatchFn(nil), + + MockScheme: NewMockSchemeFn(nil), } } @@ -249,9 +263,9 @@ func (c *MockClient) RESTMapper() meta.RESTMapper { return nil } -// Scheme returns the current runtime scheme. +// Scheme calls MockClient's MockScheme function func (c *MockClient) Scheme() *runtime.Scheme { - return nil + return c.MockScheme() } // MockStatusWriter provides mock functionality for status sub-resource diff --git a/pkg/test/integration/server.go b/pkg/test/integration/server.go index c47bcdf8b..4ab3b615c 100644 --- a/pkg/test/integration/server.go +++ b/pkg/test/integration/server.go @@ -22,7 +22,6 @@ import ( "os" "time" - "github.com/pkg/errors" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/rest" @@ -33,6 +32,7 @@ import ( // Allow auth to cloud providers _ "k8s.io/client-go/plugin/pkg/client/auth" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/resource" ) diff --git a/pkg/webhook/mutator.go b/pkg/webhook/mutator.go new file mode 100644 index 000000000..ae7441eaa --- /dev/null +++ b/pkg/webhook/mutator.go @@ -0,0 +1,65 @@ +/* +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 webhook + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" +) + +// WithMutationFns allows you to initiate the mutator with given list of mutator +// functions. +func WithMutationFns(fns ...MutateFn) MutatorOption { + return func(m *Mutator) { + m.MutationChain = fns + } +} + +// MutatorOption configures given Mutator. +type MutatorOption func(*Mutator) + +// MutateFn is a single mutating function that can be used by Mutator. +type MutateFn func(ctx context.Context, obj runtime.Object) error + +// NewMutator returns a new instance of Mutator that can be used as CustomDefaulter. +func NewMutator(opts ...MutatorOption) *Mutator { + m := &Mutator{ + MutationChain: []MutateFn{}, + } + for _, f := range opts { + f(m) + } + return m +} + +// Mutator satisfies CustomDefaulter interface with an ordered MutateFn list. +type Mutator struct { + MutationChain []MutateFn +} + +// Default executes the MutatorFns in given order. Its name might sound misleading +// since defaulting seems to be the first use case used by controller-runtime +// but MutatorFns can make any changes on given resource. +func (m *Mutator) Default(ctx context.Context, obj runtime.Object) error { + for _, f := range m.MutationChain { + if err := f(ctx, obj); err != nil { + return err + } + } + return nil +} diff --git a/pkg/webhook/mutator_test.go b/pkg/webhook/mutator_test.go new file mode 100644 index 000000000..8ad7e4c03 --- /dev/null +++ b/pkg/webhook/mutator_test.go @@ -0,0 +1,80 @@ +/* +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 webhook + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/crossplane/crossplane-runtime/pkg/test" +) + +// Mutator has to satisfy CustomDefaulter interface so that it can be used by +// controller-runtime Manager. +var _ webhook.CustomDefaulter = &Mutator{} + +func TestDefault(t *testing.T) { + type args struct { + obj runtime.Object + fns []MutateFn + } + type want struct { + err error + } + cases := map[string]struct { + reason string + args + want + }{ + "Success": { + reason: "Functions without errors should be executed successfully", + args: args{ + fns: []MutateFn{ + func(_ context.Context, _ runtime.Object) error { + return nil + }, + }, + }, + }, + "Failure": { + reason: "Functions with errors should return with error", + args: args{ + fns: []MutateFn{ + func(_ context.Context, _ runtime.Object) error { + return errBoom + }, + }, + }, + want: want{ + err: errBoom, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + v := NewMutator(WithMutationFns(tc.fns...)) + err := v.Default(context.TODO(), tc.args.obj) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nDefault(...): -want, +got\n%s\n", tc.reason, diff) + } + }) + } +} diff --git a/pkg/webhook/validator.go b/pkg/webhook/validator.go new file mode 100644 index 000000000..10b9f0232 --- /dev/null +++ b/pkg/webhook/validator.go @@ -0,0 +1,109 @@ +/* +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 webhook + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" +) + +// WithValidateCreationFns initializes the Validator with given set of creation +// validation functions. +func WithValidateCreationFns(fns ...ValidateCreateFn) ValidatorOption { + return func(v *Validator) { + v.CreationChain = fns + } +} + +// WithValidateUpdateFns initializes the Validator with given set of update +// validation functions. +func WithValidateUpdateFns(fns ...ValidateUpdateFn) ValidatorOption { + return func(v *Validator) { + v.UpdateChain = fns + } +} + +// WithValidateDeletionFns initializes the Validator with given set of deletion +// validation functions. +func WithValidateDeletionFns(fns ...ValidateDeleteFn) ValidatorOption { + return func(v *Validator) { + v.DeletionChain = fns + } +} + +// ValidatorOption allows you to configure given Validator. +type ValidatorOption func(*Validator) + +// ValidateCreateFn is function type for creation validation. +type ValidateCreateFn func(ctx context.Context, obj runtime.Object) error + +// ValidateUpdateFn is function type for update validation. +type ValidateUpdateFn func(ctx context.Context, oldObj, newObj runtime.Object) error + +// ValidateDeleteFn is function type for deletion validation. +type ValidateDeleteFn func(ctx context.Context, obj runtime.Object) error + +// NewValidator returns a new Validator with no-op defaults. +func NewValidator(opts ...ValidatorOption) *Validator { + vc := &Validator{ + CreationChain: []ValidateCreateFn{}, + UpdateChain: []ValidateUpdateFn{}, + DeletionChain: []ValidateDeleteFn{}, + } + for _, f := range opts { + f(vc) + } + return vc +} + +// Validator runs the given validation chains in order. +type Validator struct { + CreationChain []ValidateCreateFn + UpdateChain []ValidateUpdateFn + DeletionChain []ValidateDeleteFn +} + +// ValidateCreate runs functions in creation chain in order. +func (vc *Validator) ValidateCreate(ctx context.Context, obj runtime.Object) error { + for _, f := range vc.CreationChain { + if err := f(ctx, obj); err != nil { + return err + } + } + return nil +} + +// ValidateUpdate runs functions in update chain in order. +func (vc *Validator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error { + for _, f := range vc.UpdateChain { + if err := f(ctx, oldObj, newObj); err != nil { + return err + } + } + return nil +} + +// ValidateDelete runs functions in deletion chain in order. +func (vc *Validator) ValidateDelete(ctx context.Context, obj runtime.Object) error { + for _, f := range vc.DeletionChain { + if err := f(ctx, obj); err != nil { + return err + } + } + return nil +} diff --git a/pkg/webhook/validator_test.go b/pkg/webhook/validator_test.go new file mode 100644 index 000000000..c8bfb0ba2 --- /dev/null +++ b/pkg/webhook/validator_test.go @@ -0,0 +1,181 @@ +/* +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 webhook + +import ( + "context" + "testing" + + "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/google/go-cmp/cmp" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/test" +) + +// Validator has to satisfy CustomValidator interface so that it can be used by +// controller-runtime Manager. +var _ webhook.CustomValidator = &Validator{} + +var errBoom = errors.New("boom") + +func TestValidateCreate(t *testing.T) { + type args struct { + obj runtime.Object + fns []ValidateCreateFn + } + type want struct { + err error + } + cases := map[string]struct { + reason string + args + want + }{ + "Success": { + reason: "Functions without errors should be executed successfully", + args: args{ + fns: []ValidateCreateFn{ + func(_ context.Context, _ runtime.Object) error { + return nil + }, + }, + }, + }, + "Failure": { + reason: "Functions with errors should return with error", + args: args{ + fns: []ValidateCreateFn{ + func(_ context.Context, _ runtime.Object) error { + return errBoom + }, + }, + }, + want: want{ + err: errBoom, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + v := NewValidator(WithValidateCreationFns(tc.fns...)) + err := v.ValidateCreate(context.TODO(), tc.args.obj) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nValidateCreate(...): -want, +got\n%s\n", tc.reason, diff) + } + }) + } +} + +func TestValidateUpdate(t *testing.T) { + type args struct { + oldObj runtime.Object + newObj runtime.Object + fns []ValidateUpdateFn + } + type want struct { + err error + } + cases := map[string]struct { + reason string + args + want + }{ + "Success": { + reason: "Functions without errors should be executed successfully", + args: args{ + fns: []ValidateUpdateFn{ + func(_ context.Context, _, _ runtime.Object) error { + return nil + }, + }, + }, + }, + "Failure": { + reason: "Functions with errors should return with error", + args: args{ + fns: []ValidateUpdateFn{ + func(_ context.Context, _, _ runtime.Object) error { + return errBoom + }, + }, + }, + want: want{ + err: errBoom, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + v := NewValidator(WithValidateUpdateFns(tc.fns...)) + err := v.ValidateUpdate(context.TODO(), tc.args.oldObj, tc.args.newObj) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nValidateUpdate(...): -want, +got\n%s\n", tc.reason, diff) + } + }) + } +} + +func TestValidateDelete(t *testing.T) { + type args struct { + obj runtime.Object + fns []ValidateDeleteFn + } + type want struct { + err error + } + cases := map[string]struct { + reason string + args + want + }{ + "Success": { + reason: "Functions without errors should be executed successfully", + args: args{ + fns: []ValidateDeleteFn{ + func(_ context.Context, _ runtime.Object) error { + return nil + }, + }, + }, + }, + "Failure": { + reason: "Functions with errors should return with error", + args: args{ + fns: []ValidateDeleteFn{ + func(_ context.Context, _ runtime.Object) error { + return errBoom + }, + }, + }, + want: want{ + err: errBoom, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + v := NewValidator(WithValidateDeletionFns(tc.fns...)) + err := v.ValidateDelete(context.TODO(), tc.args.obj) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nValidateDelete(...): -want, +got\n%s\n", tc.reason, diff) + } + }) + } +}