diff --git a/api/cloud/v1alpha1/cluster_connection.go b/api/cloud/v1alpha1/cluster_connection.go new file mode 100644 index 0000000..0c37340 --- /dev/null +++ b/api/cloud/v1alpha1/cluster_connection.go @@ -0,0 +1,29 @@ +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +//+kubebuilder:object:root=true +//+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +//+kubebuilder:resource:scope=Cluster +//+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="Endpoint",type="string",JSONPath=".spec.hostPort",description="Temporal Cloud API server host:port" +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age" + +// ClusterConnection is the cluster-scoped analog of Connection. +type ClusterConnection struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ConnectionSpec `json:"spec,omitempty"` + Status ConnectionStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true +//+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ClusterConnectionList contains a list of ClusterConnection +type ClusterConnectionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClusterConnection `json:"items"` +} diff --git a/api/cloud/v1alpha1/connection.go b/api/cloud/v1alpha1/connection.go new file mode 100644 index 0000000..1e60faf --- /dev/null +++ b/api/cloud/v1alpha1/connection.go @@ -0,0 +1,62 @@ +package v1alpha1 + +import ( + tcorev1 "github.com/temporalio/kube-temporal/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ConnectionSpec describes the desired configuration of a Connection to a +// Temporal Cloud API server. It describes where credentials can be found for +// instantiating a Temporal Cloud client object to that Temporal Cloud API +// server. +type ConnectionSpec struct { + // HostPort is the host:port of the Temporal Cloud API server endpoint to + // connect to. + // + // Defaults to "saas-api.tmprl.cloud:443" + // + // This is a gRPC address and therefore can also support a + // special-formatted address of ":///" that will use a + // registered resolver. By default all hosts returned from the resolver + // will be used in a round-robin fashion. + // + // The "dns" resolver is registered by and used by default. + // +optional + HostPort string `json:"hostPort,omitempty"` + // TLS configures TLS behaviour for the Temporal Cloud server connection. + // +optional + TLS tcorev1.ServerTLSConfig `json:"tls,omitempty"` + // Credentials contains desired configuration for the Temporal Cloud client + // credentials. + Credentials CredentialsConfig `json:"credentials"` +} + +// ConnectionStatus defines the observed state of a Connection +type ConnectionStatus struct { + tcorev1.StatusBase `json:",inline"` +} + +//+kubebuilder:object:root=true +//+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +//+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="Endpoint",type="string",JSONPath=".spec.hostPort",description="Temporal API server host:port" +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age" + +// Connection describes a client Connection to a Temporal Cloud API server. +type Connection struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ConnectionSpec `json:"spec,omitempty"` + Status ConnectionStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true +//+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ConnectionList contains a list of Connection +type ConnectionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Connection `json:"items"` +} diff --git a/api/cloud/v1alpha1/credentials.go b/api/cloud/v1alpha1/credentials.go new file mode 100644 index 0000000..f34454a --- /dev/null +++ b/api/cloud/v1alpha1/credentials.go @@ -0,0 +1,13 @@ +package v1alpha1 + +import ( + tcorev1 "github.com/temporalio/kube-temporal/api/core/v1" +) + +// CredentialsConfig contains instructions as to where Temporal Cloud client +// credentials can be found. +type CredentialsConfig struct { + // APIKey contains the optional configuration for connecting to a Temporal + // API server using Temporal API Key authentication. + APIKey tcorev1.APIKeyCredentialsConfig `json:"apiKey"` +} diff --git a/api/cloud/v1alpha1/zz_generated.deepcopy.go b/api/cloud/v1alpha1/zz_generated.deepcopy.go index eb1a6fe..74cf85b 100644 --- a/api/cloud/v1alpha1/zz_generated.deepcopy.go +++ b/api/cloud/v1alpha1/zz_generated.deepcopy.go @@ -68,6 +68,67 @@ func (in *CertificateFilterSpec) DeepCopy() *CertificateFilterSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterConnection) DeepCopyInto(out *ClusterConnection) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterConnection. +func (in *ClusterConnection) DeepCopy() *ClusterConnection { + if in == nil { + return nil + } + out := new(ClusterConnection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterConnection) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterConnectionList) DeepCopyInto(out *ClusterConnectionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterConnection, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterConnectionList. +func (in *ClusterConnectionList) DeepCopy() *ClusterConnectionList { + if in == nil { + return nil + } + out := new(ClusterConnectionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterConnectionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CodecServerCustomErrorMessage) DeepCopyInto(out *CodecServerCustomErrorMessage) { *out = *in @@ -126,6 +187,119 @@ func (in *CodecServerSpec) DeepCopy() *CodecServerSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Connection) DeepCopyInto(out *Connection) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Connection. +func (in *Connection) DeepCopy() *Connection { + if in == nil { + return nil + } + out := new(Connection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Connection) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectionList) DeepCopyInto(out *ConnectionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Connection, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionList. +func (in *ConnectionList) DeepCopy() *ConnectionList { + if in == nil { + return nil + } + out := new(ConnectionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ConnectionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectionSpec) DeepCopyInto(out *ConnectionSpec) { + *out = *in + in.TLS.DeepCopyInto(&out.TLS) + in.Credentials.DeepCopyInto(&out.Credentials) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionSpec. +func (in *ConnectionSpec) DeepCopy() *ConnectionSpec { + if in == nil { + return nil + } + out := new(ConnectionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectionStatus) DeepCopyInto(out *ConnectionStatus) { + *out = *in + in.StatusBase.DeepCopyInto(&out.StatusBase) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionStatus. +func (in *ConnectionStatus) DeepCopy() *ConnectionStatus { + if in == nil { + return nil + } + out := new(ConnectionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CredentialsConfig) DeepCopyInto(out *CredentialsConfig) { + *out = *in + in.APIKey.DeepCopyInto(&out.APIKey) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialsConfig. +func (in *CredentialsConfig) DeepCopy() *CredentialsConfig { + if in == nil { + return nil + } + out := new(CredentialsConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EncryptionValidationSpec) DeepCopyInto(out *EncryptionValidationSpec) { *out = *in diff --git a/api/cloud/v1alpha1/zz_generated.register.go b/api/cloud/v1alpha1/zz_generated.register.go index 392b717..9f231fb 100644 --- a/api/cloud/v1alpha1/zz_generated.register.go +++ b/api/cloud/v1alpha1/zz_generated.register.go @@ -46,6 +46,10 @@ func init() { // Adds the list of known types to Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, + &ClusterConnection{}, + &ClusterConnectionList{}, + &Connection{}, + &ConnectionList{}, &Namespace{}, &NamespaceList{}, ) diff --git a/deploy/kustomize/crd/bases/cloud.temporal.io_clusterconnections.yaml b/deploy/kustomize/crd/bases/cloud.temporal.io_clusterconnections.yaml new file mode 100644 index 0000000..d64a9c7 --- /dev/null +++ b/deploy/kustomize/crd/bases/cloud.temporal.io_clusterconnections.yaml @@ -0,0 +1,263 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.21.0 + name: clusterconnections.cloud.temporal.io +spec: + group: cloud.temporal.io + names: + kind: ClusterConnection + listKind: ClusterConnectionList + plural: clusterconnections + singular: clusterconnection + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Temporal Cloud API server host:port + jsonPath: .spec.hostPort + name: Endpoint + type: string + - description: Age + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ClusterConnection is the cluster-scoped analog of Connection. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + ConnectionSpec describes the desired configuration of a Connection to a + Temporal Cloud API server. It describes where credentials can be found for + instantiating a Temporal Cloud client object to that Temporal Cloud API + server. + properties: + credentials: + description: |- + Credentials contains desired configuration for the Temporal Cloud client + credentials. + properties: + apiKey: + description: |- + APIKey contains the optional configuration for connecting to a Temporal + API server using Temporal API Key authentication. + properties: + path: + description: Path is the local file path in the Pod where + the API Key can be found. + type: string + secretRef: + description: |- + SecretRef selects the Secret key that contains the API key used for + authentication. + + The Secret must be `type: kubernetes.io/opaque` and must exist in the + same Kubernetes namespace as the Connection resource. + + SecretKeySelector encodes both: + - LocalObjectReference.Name: the name of the Secret resource + - Key: the data key within Secret.Data whose value is the API key token + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: Only one of secretRef or path may be set + rule: has(self.secretRef) != has(self.path) + required: + - apiKey + type: object + hostPort: + description: |- + HostPort is the host:port of the Temporal Cloud API server endpoint to + connect to. + + Defaults to "saas-api.tmprl.cloud:443" + + This is a gRPC address and therefore can also support a + special-formatted address of ":///" that will use a + registered resolver. By default all hosts returned from the resolver + will be used in a round-robin fashion. + + The "dns" resolver is registered by and used by default. + type: string + tls: + description: TLS configures TLS behaviour for the Temporal Cloud server + connection. + properties: + caCert: + description: |- + CACert defines where to find the certificate authority (CA) certificate + to use for a Connection. This certificate is appended to/ the system + trust store for the Connection. + + Note that this is only applicable when the Connection uses API Key or no + authentication credentials. When using Mutual TLS authentication, the CA + certificate is configured using the MutualTLSConfig struct. + properties: + path: + description: |- + Path contains the path in the local Pod filesystem where the CA + certificate file can be found. + + Use Path when the CA certificate file contents are managed by an + external system or sidecar. + type: string + secretRef: + description: |- + SecretRef references a Kubernetes Secret that is either `type: opaque` + or `type: kubernetes.io/tls`. + + When the Secret type is `opaque`, the `SecretKeySelector.type` field + should contain the key in the Secret data where the CA certificate file + contents can be found. + + When the Secret type is `kubernetes.io/tls`, the + `SecretKeySelector.type` field is ignored. The CA certificate file + contents should be at the standard `ca.crt` key location in the Secret + data. + + More information about creating a TLS secret: + https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + serverName: + description: |- + ServerName overrides the server name used to verify the Temporal server + certificate when TLS is enabled. + pattern: ^[a-zA-Z0-9.-]+$ + type: string + type: object + required: + - credentials + type: object + status: + description: ConnectionStatus defines the observed state of a Connection + properties: + conditions: + description: Conditions is the collection of Conditions associated + with the resource. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/kustomize/crd/bases/cloud.temporal.io_connections.yaml b/deploy/kustomize/crd/bases/cloud.temporal.io_connections.yaml new file mode 100644 index 0000000..23ebfb2 --- /dev/null +++ b/deploy/kustomize/crd/bases/cloud.temporal.io_connections.yaml @@ -0,0 +1,264 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.21.0 + name: connections.cloud.temporal.io +spec: + group: cloud.temporal.io + names: + kind: Connection + listKind: ConnectionList + plural: connections + singular: connection + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Temporal API server host:port + jsonPath: .spec.hostPort + name: Endpoint + type: string + - description: Age + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Connection describes a client Connection to a Temporal Cloud + API server. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + ConnectionSpec describes the desired configuration of a Connection to a + Temporal Cloud API server. It describes where credentials can be found for + instantiating a Temporal Cloud client object to that Temporal Cloud API + server. + properties: + credentials: + description: |- + Credentials contains desired configuration for the Temporal Cloud client + credentials. + properties: + apiKey: + description: |- + APIKey contains the optional configuration for connecting to a Temporal + API server using Temporal API Key authentication. + properties: + path: + description: Path is the local file path in the Pod where + the API Key can be found. + type: string + secretRef: + description: |- + SecretRef selects the Secret key that contains the API key used for + authentication. + + The Secret must be `type: kubernetes.io/opaque` and must exist in the + same Kubernetes namespace as the Connection resource. + + SecretKeySelector encodes both: + - LocalObjectReference.Name: the name of the Secret resource + - Key: the data key within Secret.Data whose value is the API key token + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: Only one of secretRef or path may be set + rule: has(self.secretRef) != has(self.path) + required: + - apiKey + type: object + hostPort: + description: |- + HostPort is the host:port of the Temporal Cloud API server endpoint to + connect to. + + Defaults to "saas-api.tmprl.cloud:443" + + This is a gRPC address and therefore can also support a + special-formatted address of ":///" that will use a + registered resolver. By default all hosts returned from the resolver + will be used in a round-robin fashion. + + The "dns" resolver is registered by and used by default. + type: string + tls: + description: TLS configures TLS behaviour for the Temporal Cloud server + connection. + properties: + caCert: + description: |- + CACert defines where to find the certificate authority (CA) certificate + to use for a Connection. This certificate is appended to/ the system + trust store for the Connection. + + Note that this is only applicable when the Connection uses API Key or no + authentication credentials. When using Mutual TLS authentication, the CA + certificate is configured using the MutualTLSConfig struct. + properties: + path: + description: |- + Path contains the path in the local Pod filesystem where the CA + certificate file can be found. + + Use Path when the CA certificate file contents are managed by an + external system or sidecar. + type: string + secretRef: + description: |- + SecretRef references a Kubernetes Secret that is either `type: opaque` + or `type: kubernetes.io/tls`. + + When the Secret type is `opaque`, the `SecretKeySelector.type` field + should contain the key in the Secret data where the CA certificate file + contents can be found. + + When the Secret type is `kubernetes.io/tls`, the + `SecretKeySelector.type` field is ignored. The CA certificate file + contents should be at the standard `ca.crt` key location in the Secret + data. + + More information about creating a TLS secret: + https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + serverName: + description: |- + ServerName overrides the server name used to verify the Temporal server + certificate when TLS is enabled. + pattern: ^[a-zA-Z0-9.-]+$ + type: string + type: object + required: + - credentials + type: object + status: + description: ConnectionStatus defines the observed state of a Connection + properties: + conditions: + description: Conditions is the collection of Conditions associated + with the resource. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/kustomize/crd/kustomization.yaml b/deploy/kustomize/crd/kustomization.yaml index a0ecfcb..5587f23 100644 --- a/deploy/kustomize/crd/kustomization.yaml +++ b/deploy/kustomize/crd/kustomization.yaml @@ -3,6 +3,8 @@ # should be run by config/default resources: - bases/cloud.temporal.io_namespaces.yaml +- bases/cloud.temporal.io_connections.yaml +- bases/cloud.temporal.io_clusterconnections.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: diff --git a/go.mod b/go.mod index a8ed7a2..cd7de3c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,10 @@ require ( github.com/go-logr/logr v1.4.4 github.com/samber/lo v1.53.0 github.com/spf13/pflag v1.0.10 - github.com/temporalio/kube-temporal v0.2.0 + github.com/stretchr/testify v1.12.1 + github.com/temporalio/kube-temporal v0.3.0 + go.temporal.io/cloud-sdk v0.7.1 + k8s.io/api v0.37.1 k8s.io/apimachinery v0.37.1 k8s.io/client-go v0.37.1 sigs.k8s.io/controller-runtime v0.25.2 @@ -14,6 +17,7 @@ require ( require ( cel.dev/expr v0.25.2 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect @@ -21,68 +25,74 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect + github.com/fsnotify/fsnotify v1.10.0 // indirect github.com/fxamacker/cbor/v2 v2.9.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v1.0.0 // indirect - github.com/go-openapi/jsonreference v1.0.0 // indirect - github.com/go-openapi/swag v0.27.1 // indirect - github.com/go-openapi/swag/cmdutils v0.27.1 // indirect - github.com/go-openapi/swag/conv v0.27.1 // indirect - github.com/go-openapi/swag/fileutils v0.27.1 // indirect - github.com/go-openapi/swag/jsonutils v0.27.1 // indirect - github.com/go-openapi/swag/loading v0.27.1 // indirect - github.com/go-openapi/swag/mangling v0.27.1 // indirect - github.com/go-openapi/swag/netutils v0.27.1 // indirect - github.com/go-openapi/swag/pools v0.27.1 // indirect - github.com/go-openapi/swag/stringutils v0.27.1 // indirect - github.com/go-openapi/swag/typeutils v0.27.1 // indirect - github.com/go-openapi/swag/yamlutils v0.27.1 // indirect + github.com/go-openapi/jsonpointer v1.0.1 // indirect + github.com/go-openapi/jsonreference v1.0.1 // indirect + github.com/go-openapi/swag v0.29.2 // indirect + github.com/go-openapi/swag/cmdutils v0.29.2 // indirect + github.com/go-openapi/swag/conv v0.29.2 // indirect + github.com/go-openapi/swag/fileutils v0.29.2 // indirect + github.com/go-openapi/swag/jsonutils v0.29.2 // indirect + github.com/go-openapi/swag/loading v0.29.2 // indirect + github.com/go-openapi/swag/mangling v0.29.2 // indirect + github.com/go-openapi/swag/netutils v0.29.2 // indirect + github.com/go-openapi/swag/pools v0.29.2 // indirect + github.com/go-openapi/swag/stringutils v0.29.2 // indirect + github.com/go-openapi/swag/typeutils v0.29.2 // indirect + github.com/go-openapi/swag/yamlutils v0.29.2 // indirect + github.com/go-openapi/testify/v2 v2.8.0 // indirect github.com/google/cel-go v0.29.2 // indirect - github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/gnostic-models v0.7.1 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.4 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/onsi/ginkgo/v2 v2.33.0 // indirect + github.com/onsi/gomega v1.43.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.24.0 // indirect + github.com/prometheus/client_golang v1.24.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.70.0 // indirect + github.com/prometheus/common v0.70.1 // indirect github.com/prometheus/procfs v0.21.1 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect - go.opentelemetry.io/otel v1.45.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 // indirect + go.opentelemetry.io/otel v1.46.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 // indirect - go.opentelemetry.io/otel/metric v1.45.0 // indirect - go.opentelemetry.io/otel/sdk v1.45.0 // indirect - go.opentelemetry.io/otel/trace v1.45.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 // indirect + go.opentelemetry.io/otel/metric v1.46.0 // indirect + go.opentelemetry.io/otel/sdk v1.46.0 // indirect + go.opentelemetry.io/otel/trace v1.46.0 // indirect go.opentelemetry.io/proto/otlp v1.11.0 // indirect + go.temporal.io/api v1.63.5 // indirect + go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect - golang.org/x/net v0.58.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect + golang.org/x/net v0.59.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/term v0.45.0 // indirect - golang.org/x/text v0.41.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/term v0.46.0 // indirect + golang.org/x/text v0.42.0 // indirect golang.org/x/time v0.15.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a // indirect - google.golang.org/grpc v1.83.2 // indirect - google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260921155816-b14227669459 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260918162117-cecb64721679 // indirect + google.golang.org/grpc v1.84.0 // indirect + google.golang.org/protobuf v1.36.12 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - k8s.io/api v0.37.1 // indirect k8s.io/apiextensions-apiserver v0.37.0 // indirect k8s.io/apiserver v0.37.0 // indirect k8s.io/component-base v0.37.0 // indirect diff --git a/go.sum b/go.sum index d3ab51e..93f66a2 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -19,14 +19,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= -github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= +github.com/fsnotify/fsnotify v1.10.0 h1:Xx/5Ydg9CeBDX/wi4VJqStNtohYjitZhhlHt4h3St1M= +github.com/fsnotify/fsnotify v1.10.0/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -36,69 +36,67 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= -github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= -github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY= -github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0= -github.com/go-openapi/swag v0.27.1 h1:VotvOLWW8q/EAxB0YdsBBGC8XYyeL1YwBj2ungAGPNg= -github.com/go-openapi/swag v0.27.1/go.mod h1:GTkJPwHfhJp6MWr4/rCh64HVI3Ofu+tcsbfjfHmTxpE= -github.com/go-openapi/swag/cmdutils v0.27.1 h1:I7sYqaWVl5mq0NEmNQkAmFDyNin9ufvMX/p2zwtQaOE= -github.com/go-openapi/swag/cmdutils v0.27.1/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= -github.com/go-openapi/swag/conv v0.27.1 h1:8wi9ZG+olmY1wXphl93EWniPtbSPkXM/feH7FgjsvrU= -github.com/go-openapi/swag/conv v0.27.1/go.mod h1:QbqMivkpKhC3g1B1GGGOJ6ANewI3S62dbzYu3Duowqs= -github.com/go-openapi/swag/fileutils v0.27.1 h1:QQqBSoi5mW4XpU85nS0mLcA+zAE6vLzrb0QkmLKf9oM= -github.com/go-openapi/swag/fileutils v0.27.1/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= -github.com/go-openapi/swag/jsonutils v0.27.1 h1:SVgK3i4USzCU5mibOOS/l4ea2h9UQXy7J7RNLTjuXjU= -github.com/go-openapi/swag/jsonutils v0.27.1/go.mod h1:tdlEpZqdcQ17uj6J4YdK9vd8It5qWMwjWXOs0tjpRlk= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1 h1:mJu3COL9WEaZVp/Kf2PRMi7tPszPEJfSr/OO75ynCs8= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= -github.com/go-openapi/swag/loading v0.27.1 h1:/DxUgDXKbBX4bcn7r9uEXfJyzN5XpiJmZplzQTjrRCY= -github.com/go-openapi/swag/loading v0.27.1/go.mod h1:jvGh3iA2+zyUUycB5fgJWzeHnhrpvGnJJM0RVE9ZShE= -github.com/go-openapi/swag/mangling v0.27.1 h1:yC9D0HyUE8gbP+BfmGx9+AA89ikwZTMjESK3OnnoaqA= -github.com/go-openapi/swag/mangling v0.27.1/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= -github.com/go-openapi/swag/netutils v0.27.1 h1:mICMFoS82F5TZ4Zy3cqmcQk+BFeCp3Uyq3Np7GI0/qU= -github.com/go-openapi/swag/netutils v0.27.1/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ= -github.com/go-openapi/swag/pools v0.27.1 h1:9LeadcMyb2GJCbXX5hVQDbZ2Lq9TL4dCs/nx1j5DO0E= -github.com/go-openapi/swag/pools v0.27.1/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE= -github.com/go-openapi/swag/stringutils v0.27.1 h1:ZXePZ0r2p1qSjo8tD3Un4vFj8+FqlCkczxDrJIhYUp8= -github.com/go-openapi/swag/stringutils v0.27.1/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= -github.com/go-openapi/swag/typeutils v0.27.1 h1:KSTdFlfnse4r6dP9IrEnwMldjE+zs71UeEB3//PtVXc= -github.com/go-openapi/swag/typeutils v0.27.1/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= -github.com/go-openapi/swag/yamlutils v0.27.1 h1:ftxv6xvXb1E3zohUc+okZ9nSqNb9StQX/FXnKZ98sQA= -github.com/go-openapi/swag/yamlutils v0.27.1/go.mod h1:bnxFIB1qewGRiZHypXGZ3fNgf13/0HfRgnS/iZBDrOo= -github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0= -github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo= -github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= -github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-openapi/jsonpointer v1.0.1 h1:2KxywRmNwJkT/FMBa3iRNHEaAxSJvjqoufQZy3au1Mg= +github.com/go-openapi/jsonpointer v1.0.1/go.mod h1:wI7ZYsFmbIi9nBXOZqgDaS/bqOchRGZjqxFli7FBYxY= +github.com/go-openapi/jsonreference v1.0.1 h1:4zJ7AmYDKNmD3aSpfPnFNCFA5E80/xMHUNKgydaLh38= +github.com/go-openapi/jsonreference v1.0.1/go.mod h1:dYplQXa6p5lXprLcJ8LE2iU7vNpXsAHDQ5ZAgL+Qx3A= +github.com/go-openapi/swag v0.29.2 h1:9n8frkcsuQRA0INU31VbxNxt1zBaHxrHYGEf5BUjFi4= +github.com/go-openapi/swag v0.29.2/go.mod h1:RkCiX1gCVXWgLOavDXE3ALJrQ/VO2ziiIzOUs3z10xo= +github.com/go-openapi/swag/cmdutils v0.29.2 h1:cXEzX/nWCODon251f+1HgL75/R4XXML5IZxgLCWMoro= +github.com/go-openapi/swag/cmdutils v0.29.2/go.mod h1:XziaSVzYqkDvpWXzASfWajw2YvJrLscq610O2XiyqJw= +github.com/go-openapi/swag/conv v0.29.2 h1:8c9shoB8l0QRSR6ymq1llHdBWDqpIgJfjTGHx3Vuhm0= +github.com/go-openapi/swag/conv v0.29.2/go.mod h1:AZS0YigTNf8qNtD6WqJz/N4RUNmpr76SyjFGkq4+p6Q= +github.com/go-openapi/swag/fileutils v0.29.2 h1:mdUL+Vw5ah1fO1AvFCoHeIyv7YZsCz2MN3KNqd/lLVI= +github.com/go-openapi/swag/fileutils v0.29.2/go.mod h1:7QKmmodjAebaq61lGVJa8ldLdGeMF4DSgYv8tXOGGo0= +github.com/go-openapi/swag/jsonutils v0.29.2 h1:uZNSD2/rJDYAfvsLYkykTzXuyxM3QpDKV9jhRHrMu6k= +github.com/go-openapi/swag/jsonutils v0.29.2/go.mod h1:ONTdNvj3Y+IXRzfa17E+Rgt2ns/qiSOYjIo2K7v2yDs= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.2 h1:w+Fd6EBOMGlC74GYGHqCLGyb3Bpams8TtNrgM/SG4jo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.2/go.mod h1:HC2egPORFzbj/gf05Zrfgz8Mgplx3oW7hgGi7pjZVhM= +github.com/go-openapi/swag/loading v0.29.2 h1:QU1ry24e6r6Shoxe380Nx0X5iKkpRO+hJffsyJ2dVlY= +github.com/go-openapi/swag/loading v0.29.2/go.mod h1:DqilDjuiJKETecsS4TRopWG1acUDsfqN39ZmQhPu6uI= +github.com/go-openapi/swag/mangling v0.29.2 h1:ltdo3K0tTP4P6zWhVVWDU9D37/YYBuMJRdZxiS2Dtog= +github.com/go-openapi/swag/mangling v0.29.2/go.mod h1:RjDi4TnItAenS3cgCXSpIl1kKbjG7il6pnmQVl/8bp8= +github.com/go-openapi/swag/netutils v0.29.2 h1:dBli6jyUa93sDS/XYWShJzf4+nnVfFKfvY9SLUaUzB8= +github.com/go-openapi/swag/netutils v0.29.2/go.mod h1:1pM6Xg/Um8E1eps3umWl8Hme3ByWXOtHsvFC0CULHUE= +github.com/go-openapi/swag/pools v0.29.2 h1:PlGoDRF8WtSyXkedZZkpW3f9wDhFf3u8KtaBcmgmh8c= +github.com/go-openapi/swag/pools v0.29.2/go.mod h1:V3lhxVT4qDYRqc2MRSSbLsTYIYV/2HlHafhyEkmzLIc= +github.com/go-openapi/swag/stringutils v0.29.2 h1:lcnBxwAaysT3bMUVBfn9V/PkfVqkurpqufKU6rTUMGk= +github.com/go-openapi/swag/stringutils v0.29.2/go.mod h1:i9cdh7sGaa0nm3CqIvH5IM5h2QClk1wns2ktKeILvRI= +github.com/go-openapi/swag/typeutils v0.29.2 h1:O7aVvkTs3pXwikgtrPLenigEMdQFgONKRooQA9pgf2I= +github.com/go-openapi/swag/typeutils v0.29.2/go.mod h1:7+GDG+uz9Ke+eVt4rClZg7wklSDp8hT/mKNh/pC26TE= +github.com/go-openapi/swag/yamlutils v0.29.2 h1:IFKFFeDnuIwzfsuWRQU+rI8iL3g4XyAYjV/RlnlxPLM= +github.com/go-openapi/swag/yamlutils v0.29.2/go.mod h1:7MGqtcrK73sxQ4ceiyIf8qiXS/s09JLMdR5esK5euYQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.7.0 h1:wPW6YRgx3+SID1yUy/Xwa17L8kFEaEKod2VRbJDZNUs= +github.com/go-openapi/testify/enable/yaml/v2 v2.7.0/go.mod h1:mI1M88etYbc3PhgHsWQK2kwvNwW5aGFqMPbmib+SGIs= +github.com/go-openapi/testify/v2 v2.8.0 h1:19QDx5b57p8KjO2E4sgs/woL4Akp+FGLRlVm2xn0Rho= +github.com/go-openapi/testify/v2 v2.8.0/go.mod h1:4gUN8jC+RqOE2qjW/vDmfyTvl5BCht4qL7oca0odrak= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k= github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= -github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.4 h1:9ZJYjPEJpcleIKcqysXOo14NLQYwu23fzmKRFpIjPjc= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.4/go.mod h1:0xydIeg2omQ9DH6zYMP24Kk57793rnLUvP8fwdbFvz8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 h1:Bd7KaOxzULLxtZ/K5s1aLbWhR0+5RToO65TXHsf3bqQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0/go.mod h1:nN7ts3dFXKtCZWc//yfkpcQNKJABg16/uDVAZpLDalo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ= -github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= +github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -109,25 +107,21 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.27.4 h1:fcEcQW/A++6aZAZQNUmNjvA9PSOzefMJBerHJ4t8v8Y= -github.com/onsi/ginkgo/v2 v2.27.4/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= -github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q= -github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/onsi/ginkgo/v2 v2.33.0 h1:C8gBA6Uc2ZEubiV+SXiu5tZnMTwEmXHgkJwGozKtZf8= +github.com/onsi/ginkgo/v2 v2.33.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.43.1 h1:vGIPFuYrIO6/0Z09s0I0QQQgFchiX4+tb1re3MScJYo= +github.com/onsi/gomega v1.43.1/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.24.0 h1:5XStIklKuAtJSNpdD3s8XJj/Yv78IQmE1kbNk87JrAI= -github.com/prometheus/client_golang v1.24.0/go.mod h1:QcsNdotprC2nS4BTM2ucbcqxd2CeXTEa9jW7zHO9iDE= +github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= +github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI= -github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY= +github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= +github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= @@ -140,83 +134,84 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/temporalio/kube-temporal v0.2.0 h1:PC7uRlY6I2auIBGy09MDtqBVNpTFpSij70R42aDUVPo= -github.com/temporalio/kube-temporal v0.2.0/go.mod h1:/PP2/B8a0YpF6ORPkIHKpdkozOdtfMLshEOouhYEC1Y= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/temporalio/kube-temporal v0.3.0 h1:9RFd81gh99kenk6cTwifbL+oyqpky0+QGwfkY05fatM= +github.com/temporalio/kube-temporal v0.3.0/go.mod h1:gJ+9ZxerlGn29Nn6VqAAXVeH73P31JuLUe3bQ+w4nwk= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= -go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= -go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU= +go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= +go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo= -go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= -go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= -go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= -go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= -go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= -go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= -go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= -go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 h1:fG5MCxGz8+2VtrN/WgqSpJFctVz24gpxj8CxkKmc8Ww= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0/go.mod h1:BmAYTn+3ysbRe+IU2msxmf5Rx3g6DHvex+tWI3LdhYI= +go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= +go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= +go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI= +go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM= +go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= +go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= +go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c= +go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= +go.temporal.io/api v1.63.5 h1:c11+kPYHkXXL3UiShPdbMD+xtvqGsbTibUA9ypmiCa4= +go.temporal.io/api v1.63.5/go.mod h1:SrlW2JMwVlDP4nRWSNznUFqnSHd+YeMDS1BkYo63HCQ= +go.temporal.io/cloud-sdk v0.7.1 h1:XZY81McyMuHSvTH7vkfjU5X/eN4eDYMVtBDvCVuIXP8= +go.temporal.io/cloud-sdk v0.7.1/go.mod h1:W2O9t9tvo3Q/LhGgYdj8JijWbN5C84os+cz/BadIHYI= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= -go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= +golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= +golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= -gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= -gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= +golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a h1:97PfJ4tCxY5C7NzzgGqQEMZmXbISdvSArNNEOoUGKBg= -google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a/go.mod h1:1brfde68Npq6+WA75c1EHWPijZEG1kMus61ygPZfn4A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a h1:qI/YMH1ep2qQtqcp00gMQyoU7mjvbhg88GJKCvfoLj0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= -google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= -google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= -google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/genproto/googleapis/api v0.0.0-20260921155816-b14227669459 h1:GS9OIt/j7c8bvBjYNgnKQysVfmV7e4jM0H8ZK95G4t8= +google.golang.org/genproto/googleapis/api v0.0.0-20260921155816-b14227669459/go.mod h1:PX5/4vemwVoXtwEcRDWwcR1/r0qrosfx3qoVADMwnVE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260918162117-cecb64721679 h1:KmqdJU4vrNcxy/6qdg3JduZtalEXrJLspVltnR1cE+8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260918162117-cecb64721679/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.37.1 h1:l6N77U7tjwB5L056bgrBTJIEdevac/naBZ3iSvDNfpM= k8s.io/api v0.37.1/go.mod h1:zSlbB1YpJ1YQlFVQy20UYll81UJSJJUMLhkhvg6Z78M= k8s.io/apiextensions-apiserver v0.37.0 h1:zRMQ3+/LIE5oZ0tVvXwYHC+dIkSP5cjNWju7AZU1LOI= diff --git a/pkg/temporal/cloud/connection/connection.go b/pkg/temporal/cloud/connection/connection.go new file mode 100644 index 0000000..1af4ae8 --- /dev/null +++ b/pkg/temporal/cloud/connection/connection.go @@ -0,0 +1,78 @@ +package connection + +import ( + "fmt" + + tcclient "go.temporal.io/cloud-sdk/cloudclient" +) + +// Connection wraps a Temporal client struct with an ability to load the client +// from various sources, including a Kubernetes Secret object or local files. +type Connection struct { + tcc *tcclient.Client + loader *Loader +} + +// Client returns the Temporal Cloud client associated with the Connection. +func (c *Connection) Client() (*tcclient.Client, error) { + if c.tcc != nil { + return c.tcc, nil + } + if c.loader == nil { + return nil, fmt.Errorf("no Loader configured for Connection") + } + + clientOpts, err := c.loader.TemporalCloudClientOptions() + if err != nil { + return nil, err + } + client, err := tcclient.New(*clientOpts) + if err != nil { + return nil, err + } + c.tcc = client + return client, nil +} + +// Loader returns the Loader associated with the Connection, if any. +func (c *Connection) Loader() *Loader { + return c.loader +} + +type newOptions struct { + loader *Loader +} + +// NewOption allows modifying the returned Connection from New. +type NewOption func(*Connection) + +// WithLoader modifies the Connection to use the supplied Loader. +func WithLoader(loader Loader) NewOption { + return func(c *Connection) { + c.loader = &loader + } +} + +// WithTemporalCloudClient overrides the Connection's Temporal Cloud Client +// with the supplied object. +func WithTemporalCloudClient(tcc *tcclient.Client) NewOption { + return func(c *Connection) { + c.tcc = tcc + } +} + +// New returns a new Connection. +func New(opts ...NewOption) (*Connection, error) { + c := Connection{} + for _, opt := range opts { + opt(&c) + } + + if (c.tcc == nil && c.loader == nil) || (c.tcc != nil && c.loader != nil) { + return nil, fmt.Errorf( + "either use WithLoader or WithTemporalCloudClient", + ) + } + + return &c, nil +} diff --git a/pkg/temporal/cloud/connection/connection_test.go b/pkg/temporal/cloud/connection/connection_test.go new file mode 100644 index 0000000..831a999 --- /dev/null +++ b/pkg/temporal/cloud/connection/connection_test.go @@ -0,0 +1,97 @@ +package connection_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/temporalio/resource-operator/pkg/temporal/cloud/connection" + "github.com/temporalio/resource-operator/pkg/version" + tcclient "go.temporal.io/cloud-sdk/cloudclient" +) + +const ( + testCloudAPIKey = "test-api-key" +) + +func newTCCOptions() *tcclient.Options { + return &tcclient.Options{ + UserAgent: version.UserAgent(), + } +} + +func TestNew(t *testing.T) { + tccOpts := newTCCOptions() + tccOpts.APIKey = testCloudAPIKey + tcc, err := tcclient.New(*tccOpts) + require.Nil(t, err) + + loader := connection.NewLoader( + connection.WithTemporalCloudClientOptions(*tccOpts), + ) + + cases := []struct { + name string + args []connection.NewOption + expTCC *tcclient.Client + expLoader *connection.Loader + expErr string + }{ + { + name: "fail if no loader or tcclient provided", + args: []connection.NewOption{}, + expErr: "either use WithLoader or WithTemporalCloudClient", + }, + { + name: "fail if both loader and tcclient provided", + args: []connection.NewOption{ + connection.WithTemporalCloudClient(tcc), + connection.WithLoader(loader), + }, + expErr: "either use WithLoader or WithTemporalCloudClient", + }, + { + name: "temporal cloud client override", + args: []connection.NewOption{ + connection.WithTemporalCloudClient(tcc), + }, + expTCC: tcc, + expLoader: nil, + }, + { + name: "loader", + args: []connection.NewOption{ + connection.WithLoader(loader), + }, + expTCC: nil, + expLoader: &loader, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(tt *testing.T) { + require := require.New(tt) + got, err := connection.New(tc.args...) + if tc.expErr != "" { + require.ErrorContains(err, tc.expErr) + return + } + require.NotNil(got) + if tc.expTCC != nil { + gotTCC, err := got.Client() + require.Nil(err) + require.NotNil(gotTCC) + require.Equal(tc.expTCC, gotTCC) + } + gotLoader := got.Loader() + if tc.expLoader == nil { + require.Nil(gotLoader) + } else { + require.NotNil(gotLoader) + gotTCCOpts, err := gotLoader.TemporalCloudClientOptions() + require.Nil(err) + expTCCOpts, err := tc.expLoader.TemporalCloudClientOptions() + require.Nil(err) + require.EqualExportedValues(expTCCOpts, gotTCCOpts) + } + }) + } +} diff --git a/pkg/temporal/cloud/connection/loader.go b/pkg/temporal/cloud/connection/loader.go new file mode 100644 index 0000000..ac38d28 --- /dev/null +++ b/pkg/temporal/cloud/connection/loader.go @@ -0,0 +1,331 @@ +package connection + +import ( + "context" + "crypto/tls" + "fmt" + "os" + "sync" + + tcorev1 "github.com/temporalio/kube-temporal/api/core/v1" + "github.com/temporalio/kube-temporal/pkg/tlsutil" + tcclient "go.temporal.io/cloud-sdk/cloudclient" + kcorev1 "k8s.io/api/core/v1" + ktypes "k8s.io/apimachinery/pkg/types" + kclient "sigs.k8s.io/controller-runtime/pkg/client" + + tcloudv1alpha1 "github.com/temporalio/resource-operator/api/cloud/v1alpha1" + "github.com/temporalio/resource-operator/pkg/version" +) + +// Loader can load a Temporal Cloud sdkclient.Client from a variety of sources, +// including a ConnectionSpec. +type Loader struct { + kc kclient.Client + kns string + + tccOpts *tcclient.Options + + connSpec *tcloudv1alpha1.ConnectionSpec +} + +// KubeClient returns the Kubernetes controller-runtime Client associated with +// the Loader. +func (l Loader) KubeClient() kclient.Client { + return l.kc +} + +// KubeNamespace returns the Kubernetes Namespace associated with the +// Loader. +func (l Loader) KubeNamespace() string { + return l.kns +} + +// TemporalCloudClientOptions returns the Temporal Cloud SDK client Options to +// use when dialing the Temporal Cloud client. +func (l *Loader) TemporalCloudClientOptions() (*tcclient.Options, error) { + if l.tccOpts != nil { + return l.tccOpts, nil + } + if l.connSpec == nil { + return nil, fmt.Errorf("ConnectionSpec required") + } + return sync.OnceValues(func() (*tcclient.Options, error) { + tccOpts, err := l.cloudClientOptionsFromConnectionSpec() + if err != nil { + return nil, err + } + l.tccOpts = tccOpts + return tccOpts, nil + })() +} + +// NewLoaderOption allows modifying the Loader returned from NewLoader. +type NewLoaderOption func(*Loader) + +// WithKubeClient allows the caller to supply a Kubernetes client to use if +// looking up credentials in Kubernetes Secrets or ConfigMaps. +func WithKubeClient(kc kclient.Client) NewLoaderOption { + return func(l *Loader) { + l.kc = kc + } +} + +// WithKubeNamespace allows the caller to supply a Kubernetes Namespace to use +// if looking up credentials in Kubernetes Secrets or ConfigMaps. +func WithKubeNamespace(ns string) NewLoaderOption { + return func(l *Loader) { + l.kns = ns + } +} + +// WithTemporalCloudClientOptions allows the caller to override the Options +// supplied to the Temporal Cloud sdkclient.New function when constructing the +// Temporal Cloud Client. +func WithTemporalCloudClientOptions(tccOpts tcclient.Options) NewLoaderOption { + return func(l *Loader) { + l.tccOpts = &tccOpts + } +} + +// WithConnectionSpec sets the Loader's ConnectionSpec which informs the Loader +// where to find various credentials and connectivity information for the +// Temporal sdkclient.Client. +func WithConnectionSpec(spec tcloudv1alpha1.ConnectionSpec) NewLoaderOption { + return func(l *Loader) { + l.connSpec = &spec + } +} + +// NewLoader returns a Loader that knows how to load a Connection from a +// variety of sources, including a ConnectionSpec. +func NewLoader(opts ...NewLoaderOption) Loader { + l := Loader{} + for _, opt := range opts { + opt(&l) + } + + return l +} + +// cloudClientOptionsFromConnectionSpec returns Temporal Cloud +// sdkclient.Options constructed from the Loader's ConnectionSpec. +func (l *Loader) cloudClientOptionsFromConnectionSpec() ( + *tcclient.Options, error, +) { + spec := l.connSpec + if spec == nil { + return nil, fmt.Errorf("ConnectionSpec required") + } + + tccOpts, err := l.cloudClientOptionsFromAPIKeyCredentialsConfig( + spec.Credentials.APIKey, + ) + if err != nil { + return nil, fmt.Errorf( + "failed getting Temporal Cloud client.Options from "+ + "APIKeyCredentialsConfig: %w", + err, + ) + } + + tccOpts.HostPort = spec.HostPort + tccOpts.UserAgent = version.UserAgent() + + var tlsCfg *tls.Config + + tlsCfg, err = l.tlsConfigFromServerTLSConfig(spec.TLS) + if err != nil { + return nil, fmt.Errorf( + "failed getting TLS Config from ServerTLSConfig: %w", err, + ) + } + + // If the TLS ServerName has been set, make sure we set it in the + // tls.Config struct in Temporal Cloud client.Options.TLSConfig. + if tlsCfg == nil { + if spec.TLS.ServerName != "" { + tlsCfg = &tls.Config{} + tlsCfg.ServerName = spec.TLS.ServerName + } + } else { + if spec.TLS.ServerName != "" { + tlsCfg.ServerName = spec.TLS.ServerName + } + } + if tlsCfg != nil { + tccOpts.TLSConfig = tlsCfg + } + return tccOpts, nil +} + +// cloudClientOptionsFromAPIKeyCredentialsConfig returns the configured +// Temporal Cloud client.Options struct from the supplied +// APIKeyCredentialsConfig. +func (l Loader) cloudClientOptionsFromAPIKeyCredentialsConfig( + cfg tcorev1.APIKeyCredentialsConfig, +) (*tcclient.Options, error) { + switch { + case cfg.SecretRef != nil: + kc := l.KubeClient() + if kc == nil { + return nil, fmt.Errorf( + "Loader requires KubeClient to load API Key from Secret", + ) + } + secretRef := cfg.SecretRef + secretName := secretRef.Name + if secretName == "" { + return nil, fmt.Errorf("APIKey.SecretRef.Name cannot be empty") + } + secretKey := secretRef.Key + if secretKey == "" { + return nil, fmt.Errorf("APIKey.SecretRef.Key cannot be empty") + } + + ctx := context.TODO() + lookup := ktypes.NamespacedName{ + Name: secretName, + Namespace: l.KubeNamespace(), + } + var secret kcorev1.Secret + err := kc.Get(ctx, lookup, &secret) + if err != nil { + return nil, fmt.Errorf( + "failed getting Secret %q referenced by "+ + "APIKey.SecretRef : %w", + secretName, err, + ) + } + apiKeyContents, ok := secret.Data[secretKey] + if !ok || len(apiKeyContents) == 0 { + return nil, fmt.Errorf( + "secret %q referenced by APIKey.SecretRef "+ + "has no entry at key %q", + secretName, secretKey, + ) + } + return &tcclient.Options{ + APIKey: string(apiKeyContents), + }, nil + case cfg.Path != "": + path := cfg.Path + // Initial check that the file at Path actually exists. We load the API + // key with dynamic credentials to allow for adapting to rotated + // tokens... + contents, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf( + "failed reading file contents from APIKey.Path: %w", + err, + ) + } + return &tcclient.Options{ + APIKey: string(contents), + }, nil + default: + return nil, fmt.Errorf("either SecretRef or Path must be non-empty") + } +} + +// tlsConfigFromServerTLSConfig returns the tls.Config constructed after +// examining the supplied tcorev1.ServerTLSConfig. +func (l Loader) tlsConfigFromServerTLSConfig( + serverTLSCfg tcorev1.ServerTLSConfig, +) ( + *tls.Config, + error, +) { + spec := l.connSpec + if spec.TLS.CACert == nil { + return nil, nil + } + caCert, err := l.caCertFromCACertConfig(*spec.TLS.CACert) + if err != nil { + return nil, err + } + rootCAs, err := tlsutil.SystemCAPoolWithCert(caCert) + if err != nil { + return nil, err + } + if rootCAs == nil { + // nothing to override, just let the system defaults be used. + return nil, nil + } + return &tls.Config{ + RootCAs: rootCAs, + }, nil +} + +// caCertFromConnectionSpec returns the CA cert after examining the supplied +// tcorev1.CACertConfig. +func (l Loader) caCertFromCACertConfig( + caCertCfg tcorev1.CACertConfig, +) ([]byte, error) { + switch { + case caCertCfg.SecretRef != nil: + secretName := caCertCfg.SecretRef.Name + if secretName == "" { + return nil, fmt.Errorf("SecretRef.Name cannot be empty") + } + kc := l.KubeClient() + if kc == nil { + return nil, fmt.Errorf( + "Loader requires KubeClient to load CA cert from Secret", + ) + } + ctx := context.TODO() + lookup := ktypes.NamespacedName{ + Name: secretName, + Namespace: l.KubeNamespace(), + } + var secret kcorev1.Secret + err := kc.Get(ctx, lookup, &secret) + if err != nil { + return nil, fmt.Errorf( + "failed reading CA secret %q: %w", secretName, err, + ) + } + var secretKey string + secretType := secret.Type + switch secretType { + case kcorev1.SecretTypeTLS: + secretKey = "ca.crt" + case kcorev1.SecretTypeOpaque, "": + secretKey = caCertCfg.SecretRef.Key + if secretKey == "" { + return nil, fmt.Errorf( + "SecretRef.Key cannot be empty for "+ + "Secret %q with opaque secret type", + secretName, + ) + } + default: + return nil, fmt.Errorf( + "unsupported secret type %q for CA certificate Secret %q", + secretType, secretName, + ) + } + caCert, ok := secret.Data[secretKey] + if !ok || len(caCert) == 0 { + return nil, fmt.Errorf( + "CA secret %q referenced by tls.caCert.secretRef "+ + "has no entry at key %q", + secretName, secretKey, + ) + } + return caCert, nil + case caCertCfg.Path != "": + caCert, err := os.ReadFile(caCertCfg.Path) + if err != nil { + return nil, fmt.Errorf( + "failed reading CA cert from path: %w", err, + ) + } + return caCert, nil + default: + return nil, fmt.Errorf( + "either CACert.SecretRef or CACert.Path must be set", + ) + } +} diff --git a/pkg/temporal/cloud/connection/loader_test.go b/pkg/temporal/cloud/connection/loader_test.go new file mode 100644 index 0000000..df77034 --- /dev/null +++ b/pkg/temporal/cloud/connection/loader_test.go @@ -0,0 +1,546 @@ +package connection_test + +import ( + "crypto/tls" + "crypto/x509" + "os" + "path/filepath" + "sync" + "testing" + + "github.com/stretchr/testify/require" + tcorev1 "github.com/temporalio/kube-temporal/api/core/v1" + "github.com/temporalio/kube-temporal/pkg/testutil" + tcloudv1alpha1 "github.com/temporalio/resource-operator/api/cloud/v1alpha1" + "github.com/temporalio/resource-operator/pkg/temporal/cloud/connection" + tcclient "go.temporal.io/cloud-sdk/cloudclient" + kcorev1 "k8s.io/api/core/v1" + kcrfake "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +const ( + tlsCAName = "test CA" + tlsServerName = "localhost" + tlsClientName = "test client" + kubeNamespace = "kube-temporal" +) + +var ( + once sync.Once +) + +var validCAKeyPair = sync.OnceValues( + func() (*testutil.TLSKeyPair, error) { + return testutil.TLSNewCAKeyPair(tlsCAName) + }, +) + +func TestLoader_ClientOptions(t *testing.T) { + tccOpts := newTCCOptions() + + cases := []struct { + name string + args []connection.NewLoaderOption + exp *tcclient.Options + expErr string + }{ + { + name: "fail if no connection spec or tcclient opts", + args: []connection.NewLoaderOption{}, + expErr: "ConnectionSpec required", + }, + { + name: "tcclient opts override provided", + args: []connection.NewLoaderOption{ + connection.WithTemporalCloudClientOptions(*tccOpts), + }, + exp: tccOpts, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(tt *testing.T) { + require := require.New(tt) + loader := connection.NewLoader(tc.args...) + got, err := loader.TemporalCloudClientOptions() + if tc.expErr != "" { + require.ErrorContains(err, tc.expErr) + return + } + require.Nil(err) + require.NotNil(got) + require.EqualExportedValues(tc.exp, got) + }) + } +} + +func TestLoader_ClientOptions_ControllerSpec_APIKeyCredentials(t *testing.T) { + customHostPort := "host:12345" + + caKP, err := validCAKeyPair() + require.Nil(t, err) + + poolWithValidCACert, err := x509.SystemCertPool() + require.Nil(t, err) + added := poolWithValidCACert.AppendCertsFromPEM(caKP.Cert) + require.True(t, added) + + tlsCACertSecretName := "tls-ca-cert-secret-name" + tlsCACertOpaqueSecretDataKey := "ca.pem" + + tlsCACertOpaqueSecret := &kcorev1.Secret{} + tlsCACertOpaqueSecret.Name = tlsCACertSecretName + tlsCACertOpaqueSecret.Data = map[string][]byte{ + tlsCACertOpaqueSecretDataKey: caKP.Cert, + } + + tlsCACertInvalidSecret := &kcorev1.Secret{} + tlsCACertInvalidSecret.Name = tlsCACertSecretName + tlsCACertInvalidSecret.Data = map[string][]byte{ + tlsCACertOpaqueSecretDataKey: []byte("invalid cert"), + } + + tlsCACertTLSSecret := &kcorev1.Secret{} + tlsCACertTLSSecret.Type = kcorev1.SecretTypeTLS + tlsCACertTLSSecret.Name = tlsCACertSecretName + tlsCACertTLSSecret.Data = map[string][]byte{ + "ca.crt": caKP.Cert, + } + apiKeySecretName := "api-key-secret-name" + apiKeySecretDataKey := "api-key-secret-data-key" + apiKeySecretDataValue := "api-key-secret-data-value" + + apiKeySecret := &kcorev1.Secret{} + apiKeySecret.Name = apiKeySecretName + apiKeySecret.Data = map[string][]byte{ + apiKeySecretDataKey: []byte(apiKeySecretDataValue), + } + + apiKeySecretWithNamespace := &kcorev1.Secret{} + apiKeySecretWithNamespace.Name = apiKeySecretName + apiKeySecretWithNamespace.Namespace = kubeNamespace + apiKeySecretWithNamespace.Data = map[string][]byte{ + apiKeySecretDataKey: []byte(apiKeySecretDataValue), + } + + apiKeyDir := t.TempDir() + apiKeyPath := filepath.Join(apiKeyDir, "api-key") + + err = os.WriteFile(apiKeyPath, []byte(apiKeySecretDataValue), 0644) + require.Nil(t, err) + + cases := []struct { + name string + args []connection.NewLoaderOption + exp *tcclient.Options + expErr string + }{ + { + name: "fail if no api key secret ref or path", + args: []connection.NewLoaderOption{ + connection.WithConnectionSpec( + tcloudv1alpha1.ConnectionSpec{}, + ), + }, + expErr: "either SecretRef or Path must be non-empty", + }, + { + name: "fail if api key secret no kube client", + args: []connection.NewLoaderOption{ + connection.WithConnectionSpec( + tcloudv1alpha1.ConnectionSpec{ + Credentials: tcloudv1alpha1.CredentialsConfig{ + APIKey: tcorev1.APIKeyCredentialsConfig{ + SecretRef: &kcorev1.SecretKeySelector{ + LocalObjectReference: kcorev1.LocalObjectReference{ + Name: apiKeySecretName, + }, + Key: apiKeySecretDataKey, + }, + }, + }, + }, + ), + }, + expErr: "Loader requires KubeClient to load API Key from Secret", + }, + { + name: "fail if api key secret no such secret", + args: []connection.NewLoaderOption{ + connection.WithKubeClient( + kcrfake.NewFakeClient(), + ), + connection.WithConnectionSpec( + tcloudv1alpha1.ConnectionSpec{ + Credentials: tcloudv1alpha1.CredentialsConfig{ + APIKey: tcorev1.APIKeyCredentialsConfig{ + SecretRef: &kcorev1.SecretKeySelector{ + LocalObjectReference: kcorev1.LocalObjectReference{ + Name: apiKeySecretName, + }, + Key: apiKeySecretDataKey, + }, + }, + }, + }, + ), + }, + expErr: "secrets \"api-key-secret-name\" not found", + }, + { + name: "fail if api key secret no such secret in namespace", + args: []connection.NewLoaderOption{ + connection.WithKubeNamespace(kubeNamespace), + connection.WithKubeClient( + // This is the non-namespaced secret, which should not + // match the lookup with the kubeNamespace override from + // the loader options. + kcrfake.NewFakeClient(apiKeySecret), + ), + connection.WithConnectionSpec( + tcloudv1alpha1.ConnectionSpec{ + Credentials: tcloudv1alpha1.CredentialsConfig{ + APIKey: tcorev1.APIKeyCredentialsConfig{ + SecretRef: &kcorev1.SecretKeySelector{ + LocalObjectReference: kcorev1.LocalObjectReference{ + Name: apiKeySecretName, + }, + Key: apiKeySecretDataKey, + }, + }, + }, + }, + ), + }, + expErr: "secrets \"api-key-secret-name\" not found", + }, + { + name: "api key secret", + args: []connection.NewLoaderOption{ + connection.WithKubeClient( + kcrfake.NewFakeClient(apiKeySecret), + ), + connection.WithConnectionSpec( + tcloudv1alpha1.ConnectionSpec{ + Credentials: tcloudv1alpha1.CredentialsConfig{ + APIKey: tcorev1.APIKeyCredentialsConfig{ + SecretRef: &kcorev1.SecretKeySelector{ + LocalObjectReference: kcorev1.LocalObjectReference{ + Name: apiKeySecretName, + }, + Key: apiKeySecretDataKey, + }, + }, + }, + }, + ), + }, + exp: func() *tcclient.Options { + tcco := newTCCOptions() + tcco.APIKey = apiKeySecretDataValue + return tcco + }(), + }, + { + name: "api key secret in namespace", + args: []connection.NewLoaderOption{ + connection.WithKubeNamespace(kubeNamespace), + connection.WithKubeClient( + kcrfake.NewFakeClient(apiKeySecretWithNamespace), + ), + connection.WithConnectionSpec( + tcloudv1alpha1.ConnectionSpec{ + Credentials: tcloudv1alpha1.CredentialsConfig{ + APIKey: tcorev1.APIKeyCredentialsConfig{ + SecretRef: &kcorev1.SecretKeySelector{ + LocalObjectReference: kcorev1.LocalObjectReference{ + Name: apiKeySecretName, + }, + Key: apiKeySecretDataKey, + }, + }, + }, + }, + ), + }, + exp: func() *tcclient.Options { + tcco := newTCCOptions() + tcco.APIKey = apiKeySecretDataValue + return tcco + }(), + }, + { + name: "api key secret with custom host port", + args: []connection.NewLoaderOption{ + connection.WithKubeClient( + kcrfake.NewFakeClient(apiKeySecret), + ), + connection.WithConnectionSpec( + tcloudv1alpha1.ConnectionSpec{ + HostPort: customHostPort, + Credentials: tcloudv1alpha1.CredentialsConfig{ + APIKey: tcorev1.APIKeyCredentialsConfig{ + SecretRef: &kcorev1.SecretKeySelector{ + LocalObjectReference: kcorev1.LocalObjectReference{ + Name: apiKeySecretName, + }, + Key: apiKeySecretDataKey, + }, + }, + }, + }, + ), + }, + exp: func() *tcclient.Options { + tcco := newTCCOptions() + tcco.HostPort = customHostPort + tcco.APIKey = apiKeySecretDataValue + return tcco + }(), + }, + { + name: "api key secret with tls server name", + args: []connection.NewLoaderOption{ + connection.WithKubeClient( + kcrfake.NewFakeClient(apiKeySecret), + ), + connection.WithConnectionSpec( + tcloudv1alpha1.ConnectionSpec{ + TLS: tcorev1.ServerTLSConfig{ + ServerName: tlsServerName, + }, + Credentials: tcloudv1alpha1.CredentialsConfig{ + APIKey: tcorev1.APIKeyCredentialsConfig{ + SecretRef: &kcorev1.SecretKeySelector{ + LocalObjectReference: kcorev1.LocalObjectReference{ + Name: apiKeySecretName, + }, + Key: apiKeySecretDataKey, + }, + }, + }, + }, + ), + }, + exp: func() *tcclient.Options { + tcco := newTCCOptions() + tcco.TLSConfig = &tls.Config{ + ServerName: tlsServerName, + } + tcco.APIKey = apiKeySecretDataValue + return tcco + }(), + }, + { + name: "fail if api key path not found", + args: []connection.NewLoaderOption{ + connection.WithConnectionSpec( + tcloudv1alpha1.ConnectionSpec{ + Credentials: tcloudv1alpha1.CredentialsConfig{ + APIKey: tcorev1.APIKeyCredentialsConfig{ + Path: "path/does/not/exist", + }, + }, + }, + ), + }, + expErr: "path/does/not/exist: no such file or directory", + }, + { + name: "api key path", + args: []connection.NewLoaderOption{ + connection.WithConnectionSpec( + tcloudv1alpha1.ConnectionSpec{ + Credentials: tcloudv1alpha1.CredentialsConfig{ + APIKey: tcorev1.APIKeyCredentialsConfig{ + Path: apiKeyPath, + }, + }, + }, + ), + }, + exp: func() *tcclient.Options { + tcco := newTCCOptions() + tcco.APIKey = apiKeySecretDataValue + return tcco + }(), + }, + { + name: "api key path with tls server name", + args: []connection.NewLoaderOption{ + connection.WithConnectionSpec( + tcloudv1alpha1.ConnectionSpec{ + TLS: tcorev1.ServerTLSConfig{ + ServerName: tlsServerName, + }, + Credentials: tcloudv1alpha1.CredentialsConfig{ + APIKey: tcorev1.APIKeyCredentialsConfig{ + Path: apiKeyPath, + }, + }, + }, + ), + }, + exp: func() *tcclient.Options { + tcco := newTCCOptions() + tcco.APIKey = apiKeySecretDataValue + tcco.TLSConfig = &tls.Config{ + ServerName: tlsServerName, + } + return tcco + }(), + }, + { + name: "fail tls CA cert secret no such secret", + args: []connection.NewLoaderOption{ + connection.WithKubeClient( + kcrfake.NewFakeClient(), + ), + connection.WithConnectionSpec( + tcloudv1alpha1.ConnectionSpec{ + Credentials: tcloudv1alpha1.CredentialsConfig{ + APIKey: tcorev1.APIKeyCredentialsConfig{ + Path: apiKeyPath, + }, + }, + TLS: tcorev1.ServerTLSConfig{ + CACert: &tcorev1.CACertConfig{ + SecretRef: &kcorev1.SecretKeySelector{ + LocalObjectReference: kcorev1.LocalObjectReference{ + Name: tlsCACertSecretName, + }, + Key: tlsCACertOpaqueSecretDataKey, + }, + }, + }, + }, + ), + }, + expErr: "secrets \"tls-ca-cert-secret-name\" not found", + }, + { + name: "fail tls CA cert secret invalid cert", + args: []connection.NewLoaderOption{ + connection.WithKubeClient( + kcrfake.NewFakeClient(tlsCACertInvalidSecret), + ), + connection.WithConnectionSpec( + tcloudv1alpha1.ConnectionSpec{ + Credentials: tcloudv1alpha1.CredentialsConfig{ + APIKey: tcorev1.APIKeyCredentialsConfig{ + Path: apiKeyPath, + }, + }, + TLS: tcorev1.ServerTLSConfig{ + CACert: &tcorev1.CACertConfig{ + SecretRef: &kcorev1.SecretKeySelector{ + LocalObjectReference: kcorev1.LocalObjectReference{ + Name: tlsCACertSecretName, + }, + Key: tlsCACertOpaqueSecretDataKey, + }, + }, + }, + }, + ), + }, + expErr: "failed appending CA cert to root CAs", + }, + { + name: "api key with tls CA cert opaque secret type", + args: []connection.NewLoaderOption{ + connection.WithKubeClient( + kcrfake.NewFakeClient(tlsCACertOpaqueSecret), + ), + connection.WithConnectionSpec( + tcloudv1alpha1.ConnectionSpec{ + Credentials: tcloudv1alpha1.CredentialsConfig{ + APIKey: tcorev1.APIKeyCredentialsConfig{ + Path: apiKeyPath, + }, + }, + TLS: tcorev1.ServerTLSConfig{ + CACert: &tcorev1.CACertConfig{ + SecretRef: &kcorev1.SecretKeySelector{ + LocalObjectReference: kcorev1.LocalObjectReference{ + Name: tlsCACertSecretName, + }, + Key: tlsCACertOpaqueSecretDataKey, + }, + }, + }, + }, + ), + }, + exp: func() *tcclient.Options { + tcco := newTCCOptions() + tcco.APIKey = apiKeySecretDataValue + tcco.TLSConfig = &tls.Config{ + RootCAs: poolWithValidCACert, + } + return tcco + }(), + }, + { + name: "api key with tls CA cert tls secret tpe", + args: []connection.NewLoaderOption{ + connection.WithKubeClient( + kcrfake.NewFakeClient(tlsCACertTLSSecret), + ), + connection.WithConnectionSpec( + tcloudv1alpha1.ConnectionSpec{ + Credentials: tcloudv1alpha1.CredentialsConfig{ + APIKey: tcorev1.APIKeyCredentialsConfig{ + Path: apiKeyPath, + }, + }, + TLS: tcorev1.ServerTLSConfig{ + CACert: &tcorev1.CACertConfig{ + SecretRef: &kcorev1.SecretKeySelector{ + LocalObjectReference: kcorev1.LocalObjectReference{ + Name: tlsCACertSecretName, + }, + // NOTE(jaypipes): Deliberately leave Key + // field empty since this Secret is of type + // kubernetes.io/tls and we want to verify + // the autoselecting of the fixed `ca.crt` + // data key. + }, + }, + }, + }, + ), + }, + exp: func() *tcclient.Options { + tcco := newTCCOptions() + tcco.APIKey = apiKeySecretDataValue + tcco.TLSConfig = &tls.Config{ + RootCAs: poolWithValidCACert, + } + return tcco + }(), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(tt *testing.T) { + require := require.New(tt) + loader := connection.NewLoader(tc.args...) + got, err := loader.TemporalCloudClientOptions() + if tc.expErr != "" { + require.ErrorContains(err, tc.expErr) + return + } + require.Nil(err) + require.NotNil(got) + require.EqualExportedValues(tc.exp, got) + + if tc.exp.TLSConfig != nil { + require.NotNil(got.TLSConfig) + require.True( + tc.exp.TLSConfig.RootCAs.Equal( + got.TLSConfig.RootCAs, + ), + ) + } + }) + } +} diff --git a/pkg/version/version.go b/pkg/version/version.go index e5f8db1..4d564b1 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -1,8 +1,23 @@ package version +const ( + AppName = "temporal-resource-operator" + testingVersion = "v0-testing" +) + // These variables get populated at build time... var ( GitVersion string GitCommit string BuildDate string ) + +// UserAgent returns the HTTP User-Agent header value that the operator +// attaches to all API requests to Temporal Cloud. +func UserAgent() string { + version := GitVersion + if version == "" { + version = testingVersion + } + return AppName + "/" + version +} diff --git a/test/install/check.yaml b/test/install/check.yaml index 4a46c80..9238c2e 100644 --- a/test/install/check.yaml +++ b/test/install/check.yaml @@ -13,6 +13,16 @@ tests: require: len: 1 + - name: ensure-connection-crd + kube.get: crds/connections.cloud.temporal.io + require: + len: 1 + + - name: ensure-clusterconnection-crd + kube.get: crds/clusterconnections.cloud.temporal.io + require: + len: 1 + - name: ensure-serviceaccount kube.get: serviceaccounts/temporal-resource-operator require: