diff --git a/api/v1alpha1/upgradeinsights_types.go b/api/v1alpha1/upgradeinsights_types.go new file mode 100644 index 00000000..bfc00209 --- /dev/null +++ b/api/v1alpha1/upgradeinsights_types.go @@ -0,0 +1,136 @@ +package v1alpha1 + +import ( + "time" + + console "github.com/pluralsh/console/go/client" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func init() { + SchemeBuilder.Register(&UpgradeInsights{}, &UpgradeInsightsList{}) +} + +const ( + defaultReconcileInterval = 10 * time.Minute +) + +// UpgradeInsightsList contains a list of UpgradeInsights +// +kubebuilder:object:root=true +type UpgradeInsightsList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []UpgradeInsights `json:"items"` +} + +// UpgradeInsights is the Schema for the UpgradeInsights API +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="ID",type="string",JSONPath=".status.id",description="ID of the UpgradeInsights in the Console API." +type UpgradeInsights struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec UpgradeInsightsSpec `json:"spec,omitempty"` + Status Status `json:"status,omitempty"` +} + +func (in *UpgradeInsights) SetCondition(condition metav1.Condition) { + meta.SetStatusCondition(&in.Status.Conditions, condition) +} + +type UpgradeInsightsDistro string + +type UpgradeInsightsSpec struct { + // Distro defines which provider API should be used to fetch latest upgrade insights. + // If not provided, we get the distro from the Plural API cluster tied to this operator deploy token. + // +kubebuilder:validation:Enum=EKS + // +kubebuilder:validation:Optional + Distro *console.ClusterDistro `json:"distro,omitempty"` + + // ClusterName is your cloud provider cluster identifier (usually name) that is used + // to fetch latest upgrade insights information from the cloud provider API. + // If not provided, we get the cluster name from the Plural API cluster tied to this + // operator deploy token and assume that it is the same as the cluster name in your cloud provider. + // +kubebuilder:validation:Optional + ClusterName *string `json:"clusterName,omitempty"` + + // Interval defines how often should the upgrade insights information be fetched. + // +kubebuilder:default="10m" + // +kubebuilder:validation:Optional + Interval *string `json:"interval,omitempty"` + + // Credentials allow overriding default provider credentials bound to the operator. + // +kubebuilder:validation:Optional + Credentials *ProviderCredentials `json:"credentials,omitempty"` +} + +func (in *UpgradeInsightsSpec) GetDistro(defaultDistro *console.ClusterDistro) *console.ClusterDistro { + if in.Distro != nil { + return in.Distro + } + + return defaultDistro +} +func (in *UpgradeInsightsSpec) GetClusterName(defaultClusterName string) string { + if in.ClusterName != nil { + return *in.ClusterName + } + + return defaultClusterName +} + +func (in *UpgradeInsightsSpec) GetInterval() time.Duration { + if in.Interval == nil { + return defaultReconcileInterval + } + + interval, err := time.ParseDuration(*in.Interval) + if err != nil { + return defaultReconcileInterval + } + + return interval +} + +type ProviderCredentials struct { + // AWS defines attributes required to auth with AWS API. + // +kubebuilder:validation:Optional + AWS *AWSProviderCredentials `json:"aws,omitempty"` +} + +type AWSProviderCredentials struct { + // Region is the name of the AWS region cluster lives in. + // +kubebuilder:validation:Required + Region string `json:"region"` + + // AccessKeyID is your access key ID used to authenticate against AWS API. + // +kubebuilder:validation:Required + AccessKeyID string `json:"accessKeyID"` + + // SecretAccessKeyRef is a reference to the secret that contains secret access key. + // Since UpgradeInsights is a cluster-scoped resource we can't use local reference. + // + // SecretAccessKey must be stored in a key named "secretAccessKey". + // + // An example secret can look like this: + // apiVersion: v1 + // kind: Secret + // metadata: + // name: eks-credentials + // namespace: upgrade-insights-test + // stringData: + // secretAccessKey: "changeme" + // + // Then it can be referenced like this: + // ... + // secretAccessKeyRef: + // name: eks-credentials + // namespace: upgrade-insights-test + // + // +kubebuilder:validation:Required + SecretAccessKeyRef corev1.SecretReference `json:"secretAccessKeyRef"` +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8f48285d..6cc6cc1d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -28,6 +28,22 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSProviderCredentials) DeepCopyInto(out *AWSProviderCredentials) { + *out = *in + out.SecretAccessKeyRef = in.SecretAccessKeyRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSProviderCredentials. +func (in *AWSProviderCredentials) DeepCopy() *AWSProviderCredentials { + if in == nil { + return nil + } + out := new(AWSProviderCredentials) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AgentHelmConfiguration) DeepCopyInto(out *AgentHelmConfiguration) { *out = *in @@ -440,6 +456,26 @@ func (in *PipelineGateStatus) DeepCopy() *PipelineGateStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderCredentials) DeepCopyInto(out *ProviderCredentials) { + *out = *in + if in.AWS != nil { + in, out := &in.AWS, &out.AWS + *out = new(AWSProviderCredentials) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderCredentials. +func (in *ProviderCredentials) DeepCopy() *ProviderCredentials { + if in == nil { + return nil + } + out := new(ProviderCredentials) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Status) DeepCopyInto(out *Status) { *out = *in @@ -472,6 +508,100 @@ func (in *Status) DeepCopy() *Status { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpgradeInsights) DeepCopyInto(out *UpgradeInsights) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpgradeInsights. +func (in *UpgradeInsights) DeepCopy() *UpgradeInsights { + if in == nil { + return nil + } + out := new(UpgradeInsights) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *UpgradeInsights) 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 *UpgradeInsightsList) DeepCopyInto(out *UpgradeInsightsList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]UpgradeInsights, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpgradeInsightsList. +func (in *UpgradeInsightsList) DeepCopy() *UpgradeInsightsList { + if in == nil { + return nil + } + out := new(UpgradeInsightsList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *UpgradeInsightsList) 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 *UpgradeInsightsSpec) DeepCopyInto(out *UpgradeInsightsSpec) { + *out = *in + if in.Distro != nil { + in, out := &in.Distro, &out.Distro + *out = new(client.ClusterDistro) + **out = **in + } + if in.ClusterName != nil { + in, out := &in.ClusterName, &out.ClusterName + *out = new(string) + **out = **in + } + if in.Interval != nil { + in, out := &in.Interval, &out.Interval + *out = new(string) + **out = **in + } + if in.Credentials != nil { + in, out := &in.Credentials, &out.Credentials + *out = new(ProviderCredentials) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpgradeInsightsSpec. +func (in *UpgradeInsightsSpec) DeepCopy() *UpgradeInsightsSpec { + if in == nil { + return nil + } + out := new(UpgradeInsightsSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VClusterHelmConfiguration) DeepCopyInto(out *VClusterHelmConfiguration) { *out = *in diff --git a/cmd/agent/main.go b/cmd/agent/main.go index f09b85ce..8b9168c3 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -187,6 +187,14 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "VirtualCluster") } + if err = (&controller.UpgradeInsightsController{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ConsoleClient: ctrlMgr.GetClient(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "UpgradeInsights") + } + statusController, err := controller.NewStatusReconciler(mgr.GetClient()) if err != nil { setupLog.Error(err, "unable to create controller", "controller", "StatusController") diff --git a/config/crd/bases/deployments.plural.sh_upgradeinsights.yaml b/config/crd/bases/deployments.plural.sh_upgradeinsights.yaml new file mode 100644 index 00000000..8a9ee35c --- /dev/null +++ b/config/crd/bases/deployments.plural.sh_upgradeinsights.yaml @@ -0,0 +1,197 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: upgradeinsights.deployments.plural.sh +spec: + group: deployments.plural.sh + names: + kind: UpgradeInsights + listKind: UpgradeInsightsList + plural: upgradeinsights + singular: upgradeinsights + scope: Cluster + versions: + - additionalPrinterColumns: + - description: ID of the UpgradeInsights in the Console API. + jsonPath: .status.id + name: ID + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: UpgradeInsights is the Schema for the UpgradeInsights API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + clusterName: + description: |- + ClusterName is your cloud provider cluster identifier (usually name) that is used + to fetch latest upgrade insights information from the cloud provider API. + If not provided, we get the cluster name from the Plural API cluster tied to this + operator deploy token and assume that it is the same as the cluster name in your cloud provider. + type: string + credentials: + description: Credentials allow overriding default provider credentials + bound to the operator. + properties: + aws: + description: AWS defines attributes required to auth with AWS + API. + properties: + accessKeyID: + description: AccessKeyID is your access key ID used to authenticate + against AWS API. + type: string + region: + description: Region is the name of the AWS region cluster + lives in. + type: string + secretAccessKeyRef: + description: "SecretAccessKeyRef is a reference to the secret + that contains secret access key.\nSince UpgradeInsights + is a cluster-scoped resource we can't use local reference.\n\n\nSecretAccessKey + must be stored in a key named \"secretAccessKey\".\n\n\nAn + example secret can look like this:\n\tapiVersion: v1\n\tkind: + Secret\n\tmetadata:\n name: eks-credentials\n namespace: + upgrade-insights-test\n\tstringData:\n secretAccessKey: + \"changeme\"\n\n\nThen it can be referenced like this:\n + \ ...\n secretAccessKeyRef:\n name: eks-credentials\n + \ namespace: upgrade-insights-test" + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which + the secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - accessKeyID + - region + - secretAccessKeyRef + type: object + type: object + distro: + description: |- + Distro defines which provider API should be used to fetch latest upgrade insights. + If not provided, we get the distro from the Plural API cluster tied to this operator deploy token. + enum: + - EKS + type: string + interval: + default: 10m + description: Interval defines how often should the upgrade insights + information be fetched. + type: string + type: object + status: + properties: + conditions: + description: Represents the observations of a PrAutomation's current + state. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + id: + description: ID of the resource in the Console API. + type: string + sha: + description: SHA of last applied configuration. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/docs/api.md b/docs/api.md index 780f091d..57089f9e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -11,9 +11,107 @@ Package v1alpha1 contains API Schema definitions for the deployments v1alpha1 AP ### Resource Types - [CustomHealth](#customhealth) - [PipelineGate](#pipelinegate) +- [UpgradeInsights](#upgradeinsights) +- [VirtualCluster](#virtualcluster) +#### AWSProviderCredentials + + + + + + + +_Appears in:_ +- [ProviderCredentials](#providercredentials) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `region` _string_ | Region is the name of the AWS region cluster lives in. | | Required: {}
| +| `accessKeyID` _string_ | AccessKeyID is your access key ID used to authenticate against AWS API. | | Required: {}
| +| `secretAccessKeyRef` _[SecretReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#secretreference-v1-core)_ | SecretAccessKeyRef is a reference to the secret that contains secret access key.
Since UpgradeInsights is a cluster-scoped resource we can't use local reference.
SecretAccessKey must be stored in a key named "secretAccessKey".
An example secret can look like this:
apiVersion: v1
kind: Secret
metadata:
name: eks-credentials
namespace: upgrade-insights-test
stringData:
secretAccessKey: "changeme"
Then it can be referenced like this:
...
secretAccessKeyRef:
name: eks-credentials
namespace: upgrade-insights-test | | Required: {}
| + + +#### AgentHelmConfiguration + + + + + + + +_Appears in:_ +- [HelmSpec](#helmspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `chartName` _string_ | ChartName is a helm chart name. | | | +| `repoUrl` _string_ | RepoUrl is a url that points to this helm chart. | | Optional: {}
Type: string
| +| `values` _[RawExtension](https://pkg.go.dev/k8s.io/apimachinery/pkg/runtime#RawExtension)_ | Values allows defining arbitrary YAML values to pass to the helm as values.yaml file.
Use only one of:
- Values
- ValuesSecretRef
- ValuesConfigMapRef | | Optional: {}
| +| `valuesSecretRef` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#secretkeyselector-v1-core)_ | ValuesSecretRef fetches helm values from a secret in this cluster.
Use only one of:
- Values
- ValuesSecretRef
- ValuesConfigMapRef | | Optional: {}
| +| `valuesConfigMapRef` _[ConfigMapKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#configmapkeyselector-v1-core)_ | ValuesConfigMapRef fetches helm values from a config map in this cluster.
Use only one of:
- Values
- ValuesSecretRef
- ValuesConfigMapRef | | Optional: {}
| + + +#### Binding + + + +Binding ... + + + +_Appears in:_ +- [Bindings](#bindings) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `id` _string_ | | | Optional: {}
| +| `UserID` _string_ | | | Optional: {}
| +| `userEmail` _string_ | | | Optional: {}
| +| `groupID` _string_ | | | Optional: {}
| +| `groupName` _string_ | | | Optional: {}
| + + +#### Bindings + + + +Bindings represents a policy bindings that +can be used to define read/write permissions +to this resource for users/groups in the system. + + + +_Appears in:_ +- [ClusterSpec](#clusterspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `read` _[Binding](#binding) array_ | Read bindings. | | Optional: {}
| +| `write` _[Binding](#binding) array_ | Write bindings. | | Optional: {}
| + + +#### ClusterSpec + + + + + + + +_Appears in:_ +- [VirtualClusterSpec](#virtualclusterspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `handle` _string_ | Handle is a short, unique human-readable name used to identify this cluster.
Does not necessarily map to the cloud resource name. | | Optional: {}
| +| `tags` _object (keys:string, values:string)_ | Tags used to filter clusters. | | Optional: {}
| +| `metadata` _[RawExtension](https://pkg.go.dev/k8s.io/apimachinery/pkg/runtime#RawExtension)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | Optional: {}
| +| `bindings` _[Bindings](#bindings)_ | Bindings contain read and write policies of this cluster | | Optional: {}
| + + @@ -100,6 +198,46 @@ _Appears in:_ + + +#### HelmConfiguration + + + + + + + +_Appears in:_ +- [AgentHelmConfiguration](#agenthelmconfiguration) +- [VClusterHelmConfiguration](#vclusterhelmconfiguration) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `chartName` _string_ | ChartName is a helm chart name. | | | +| `repoUrl` _string_ | RepoUrl is a url that points to this helm chart. | | Optional: {}
Type: string
| +| `values` _[RawExtension](https://pkg.go.dev/k8s.io/apimachinery/pkg/runtime#RawExtension)_ | Values allows defining arbitrary YAML values to pass to the helm as values.yaml file.
Use only one of:
- Values
- ValuesSecretRef
- ValuesConfigMapRef | | Optional: {}
| +| `valuesSecretRef` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#secretkeyselector-v1-core)_ | ValuesSecretRef fetches helm values from a secret in this cluster.
Use only one of:
- Values
- ValuesSecretRef
- ValuesConfigMapRef | | Optional: {}
| +| `valuesConfigMapRef` _[ConfigMapKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#configmapkeyselector-v1-core)_ | ValuesConfigMapRef fetches helm values from a config map in this cluster.
Use only one of:
- Values
- ValuesSecretRef
- ValuesConfigMapRef | | Optional: {}
| + + +#### HelmSpec + + + + + + + +_Appears in:_ +- [VirtualClusterSpec](#virtualclusterspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `agent` _[AgentHelmConfiguration](#agenthelmconfiguration)_ | Agent allows configuring agent specific helm chart options. | | Optional: {}
| +| `vcluster` _[VClusterHelmConfiguration](#vclusterhelmconfiguration)_ | VCluster allows configuring vcluster specific helm chart options. | | Optional: {}
| + + #### PipelineGate @@ -139,3 +277,136 @@ _Appears in:_ +#### ProviderCredentials + + + + + + + +_Appears in:_ +- [UpgradeInsightsSpec](#upgradeinsightsspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `aws` _[AWSProviderCredentials](#awsprovidercredentials)_ | AWS defines attributes required to auth with AWS API. | | Optional: {}
| + + +#### Status + + + + + + + +_Appears in:_ +- [VirtualClusterStatus](#virtualclusterstatus) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `id` _string_ | ID of the resource in the Console API. | | Optional: {}
Type: string
| +| `sha` _string_ | SHA of last applied configuration. | | Optional: {}
Type: string
| +| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#condition-v1-meta) array_ | Represents the observations of a PrAutomation's current state. | | | + + +#### UpgradeInsights + + + +UpgradeInsights is the Schema for the UpgradeInsights API + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `deployments.plural.sh/v1alpha1` | | | +| `kind` _string_ | `UpgradeInsights` | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `spec` _[UpgradeInsightsSpec](#upgradeinsightsspec)_ | | | | + + + + +#### UpgradeInsightsSpec + + + + + + + +_Appears in:_ +- [UpgradeInsights](#upgradeinsights) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `distro` _[ClusterDistro](#clusterdistro)_ | Distro defines which provider API should be used to fetch latest upgrade insights.
If not provided, we get the distro from the Plural API cluster tied to this operator deploy token. | | Enum: [EKS]
Optional: {}
| +| `clusterName` _string_ | ClusterName is your cloud provider cluster identifier (usually name) that is used
to fetch latest upgrade insights information from the cloud provider API.
If not provided, we get the cluster name from the Plural API cluster tied to this
operator deploy token and assume that it is the same as the cluster name in your cloud provider. | | Optional: {}
| +| `interval` _string_ | Interval defines how often should the upgrade insights information be fetched. | 10m | Optional: {}
| +| `credentials` _[ProviderCredentials](#providercredentials)_ | Credentials allow overriding default provider credentials bound to the operator. | | Optional: {}
| + + +#### VClusterHelmConfiguration + + + + + + + +_Appears in:_ +- [HelmSpec](#helmspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `chartName` _string_ | ChartName is a helm chart name. | | | +| `repoUrl` _string_ | RepoUrl is a url that points to this helm chart. | | Optional: {}
Type: string
| +| `values` _[RawExtension](https://pkg.go.dev/k8s.io/apimachinery/pkg/runtime#RawExtension)_ | Values allows defining arbitrary YAML values to pass to the helm as values.yaml file.
Use only one of:
- Values
- ValuesSecretRef
- ValuesConfigMapRef | | Optional: {}
| +| `valuesSecretRef` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#secretkeyselector-v1-core)_ | ValuesSecretRef fetches helm values from a secret in this cluster.
Use only one of:
- Values
- ValuesSecretRef
- ValuesConfigMapRef | | Optional: {}
| +| `valuesConfigMapRef` _[ConfigMapKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#configmapkeyselector-v1-core)_ | ValuesConfigMapRef fetches helm values from a config map in this cluster.
Use only one of:
- Values
- ValuesSecretRef
- ValuesConfigMapRef | | Optional: {}
| + + +#### VirtualCluster + + + +VirtualCluster is the Schema for the virtual cluster API + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `deployments.plural.sh/v1alpha1` | | | +| `kind` _string_ | `VirtualCluster` | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `spec` _[VirtualClusterSpec](#virtualclusterspec)_ | Spec ... | | Required: {}
| + + +#### VirtualClusterSpec + + + + + + + +_Appears in:_ +- [VirtualCluster](#virtualcluster) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `kubeconfigRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#localobjectreference-v1-core)_ | KubeconfigRef is a reference to the secret created by the
vcluster helm chart. It contains kubeconfig with information
on how to access created virtual cluster. | | Required: {}
| +| `credentialsRef` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#secretkeyselector-v1-core)_ | CredentialsRef is a reference to the secret pointing to the
key that holds Console API access token. It allows to communicate
with the standard Console API. | | Required: {}
| +| `cluster` _[ClusterSpec](#clusterspec)_ | Cluster is a simplified representation of the Console API cluster
object. See [ClusterSpec] for more information. | | Optional: {}
| +| `external` _boolean_ | External marks this virtual cluster as external one, meaning
that the vcluster deployment will not be automatically created.
User has to pre-provision vcluster and provide a valid KubeconfigRef
pointing to an existing vcluster installation. | | Optional: {}
| +| `helm` _[HelmSpec](#helmspec)_ | Helm allows configuring helm chart options of both agent and vcluster.
It is then deployed by the [VirtualCluster] CRD controller. | | Optional: {}
| + + + + diff --git a/go.mod b/go.mod index 2066d8b6..e5ab3279 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,10 @@ require ( github.com/Masterminds/sprig/v3 v3.2.3 github.com/Yamashou/gqlgenc v0.23.2 github.com/argoproj/argo-rollouts v1.6.6 + github.com/aws/aws-sdk-go-v2 v1.30.5 + github.com/aws/aws-sdk-go-v2/config v1.27.33 + github.com/aws/aws-sdk-go-v2/credentials v1.17.32 + github.com/aws/aws-sdk-go-v2/service/eks v1.48.4 github.com/elastic/crd-ref-docs v0.0.12 github.com/evanphx/json-patch v5.7.0+incompatible github.com/fluxcd/flagger v1.35.0 @@ -23,7 +27,7 @@ require ( github.com/open-policy-agent/gatekeeper/v3 v3.15.1 github.com/orcaman/concurrent-map/v2 v2.0.1 github.com/pkg/errors v0.9.1 - github.com/pluralsh/console/go/client v1.14.0 + github.com/pluralsh/console/go/client v1.17.0 github.com/pluralsh/controller-reconcile-helper v0.0.4 github.com/pluralsh/gophoenix v0.1.3-0.20231201014135-dff1b4309e34 github.com/pluralsh/polly v0.1.10 @@ -90,6 +94,16 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/ashanbrown/forbidigo v1.6.0 // indirect github.com/ashanbrown/makezero v1.1.1 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect + github.com/aws/smithy-go v1.20.4 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bkielbasa/cyclop v1.2.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect @@ -200,6 +214,7 @@ require ( github.com/jinzhu/copier v0.3.5 // indirect github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect github.com/jjti/go-spancheck v0.6.2 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmoiron/sqlx v1.3.5 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/go.sum b/go.sum index d47cd27c..9ae6693a 100644 --- a/go.sum +++ b/go.sum @@ -89,6 +89,34 @@ github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8ger github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= +github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g= +github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= +github.com/aws/aws-sdk-go-v2/config v1.27.33 h1:Nof9o/MsmH4oa0s2q9a0k7tMz5x/Yj5k06lDODWz3BU= +github.com/aws/aws-sdk-go-v2/config v1.27.33/go.mod h1:kEqdYzRb8dd8Sy2pOdEbExTTF5v7ozEXX0McgPE7xks= +github.com/aws/aws-sdk-go-v2/credentials v1.17.32 h1:7Cxhp/BnT2RcGy4VisJ9miUPecY+lyE9I8JvcZofn9I= +github.com/aws/aws-sdk-go-v2/credentials v1.17.32/go.mod h1:P5/QMF3/DCHbXGEGkdbilXHsyTBX5D3HSwcrSc9p20I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/eks v1.48.4 h1:rgYF107dG64XdYhQ1N0ac2G+8L3I+fD4Vsw8zz9wOKA= +github.com/aws/aws-sdk-go-v2/service/eks v1.48.4/go.mod h1:9dn8p15siUL80NCTPVNd+YvEpVTmWO+rboGx6qOMBa0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 h1:pIaGg+08llrP7Q5aiz9ICWbY8cqhTkyy+0SHvfzQpTc= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.7/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 h1:/Cfdu0XV3mONYKaOt1Gr0k1KvQzkzPyiKUdlWJqy+J4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 h1:NKTa1eqZYw8tiHSRGpP0VtTdub/8KNk8sDkNPFaOKDE= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.7/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o= +github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= +github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -445,6 +473,10 @@ github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9B github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= github.com/jjti/go-spancheck v0.6.2 h1:iYtoxqPMzHUPp7St+5yA8+cONdyXD3ug6KK15n7Pklk= github.com/jjti/go-spancheck v0.6.2/go.mod h1:+X7lvIrR5ZdUTkxFYqzJ0abr8Sb5LOo80uOhWNqIrYA= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= @@ -640,8 +672,8 @@ github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rK github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pluralsh/console/go/client v1.14.0 h1:ObS2VGw/ZhlqlgKDBLMFzQ0wm0uIkQ5BKPgFLtglIxg= -github.com/pluralsh/console/go/client v1.14.0/go.mod h1:lpoWASYsM9keNePS3dpFiEisUHEfObIVlSL3tzpKn8k= +github.com/pluralsh/console/go/client v1.17.0 h1:ExP+HUWa94e8cIbFY+6ARBq33oC87xpxTVw3+eZS7+I= +github.com/pluralsh/console/go/client v1.17.0/go.mod h1:lpoWASYsM9keNePS3dpFiEisUHEfObIVlSL3tzpKn8k= github.com/pluralsh/controller-reconcile-helper v0.0.4 h1:1o+7qYSyoeqKFjx+WgQTxDz4Q2VMpzprJIIKShxqG0E= github.com/pluralsh/controller-reconcile-helper v0.0.4/go.mod h1:AfY0gtteD6veBjmB6jiRx/aR4yevEf6K0M13/pGan/s= github.com/pluralsh/gophoenix v0.1.3-0.20231201014135-dff1b4309e34 h1:ab2PN+6if/Aq3/sJM0AVdy1SYuMAnq4g20VaKhTm/Bw= diff --git a/internal/controller/customhealth_controller.go b/internal/controller/customhealth_controller.go index 1f84fe5c..01da7232 100644 --- a/internal/controller/customhealth_controller.go +++ b/internal/controller/customhealth_controller.go @@ -66,7 +66,7 @@ func (r *CustomHealthReconciler) Reconcile(ctx context.Context, req ctrl.Request scope, err := NewClusterScope(ctx, r.Client, script) if err != nil { logger.Error(err, "Failed to create cluster scope") - utils.MarkCondition(script.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, "%s", err.Error()) + utils.MarkCondition(script.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } defer func() { diff --git a/internal/controller/upgradeinsights_cloudprovider.go b/internal/controller/upgradeinsights_cloudprovider.go new file mode 100644 index 00000000..cb24c4c1 --- /dev/null +++ b/internal/controller/upgradeinsights_cloudprovider.go @@ -0,0 +1,217 @@ +package controller + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + awscredentials "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/eks" + "github.com/aws/aws-sdk-go-v2/service/eks/types" + console "github.com/pluralsh/console/go/client" + "github.com/pluralsh/polly/algorithms" + "github.com/samber/lo" + corev1 "k8s.io/api/core/v1" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/pluralsh/deployment-operator/api/v1alpha1" +) + +type CloudProvider interface { + UpgradeInsights(context.Context, v1alpha1.UpgradeInsights) ([]console.UpgradeInsightAttributes, error) +} + +type EKSCloudProvider struct { + kubeClient runtimeclient.Client + clusterName string +} + +func (in *EKSCloudProvider) UpgradeInsights(ctx context.Context, ui v1alpha1.UpgradeInsights) ([]console.UpgradeInsightAttributes, error) { + client, err := in.client(ctx, ui) + if err != nil { + return nil, err + } + + insights, err := in.listInsights(ctx, client, ui) + if err != nil { + return nil, err + } + + return algorithms.Map(insights, func(insight *types.Insight) console.UpgradeInsightAttributes { + var refreshedAt *string + if insight.LastRefreshTime != nil { + refreshedAt = lo.ToPtr(insight.LastRefreshTime.Format(time.RFC3339)) + } + + var transitionedAt *string + if insight.LastTransitionTime != nil { + transitionedAt = lo.ToPtr(insight.LastTransitionTime.Format(time.RFC3339)) + } + + return console.UpgradeInsightAttributes{ + Name: lo.FromPtr(insight.Name), + Version: insight.KubernetesVersion, + Description: insight.Description, + Status: in.fromInsightStatus(insight.InsightStatus), + Details: in.toInsightDetails(insight), + RefreshedAt: refreshedAt, + TransitionedAt: transitionedAt, + } + }), nil +} + +func (in *EKSCloudProvider) listInsights(ctx context.Context, client *eks.Client, ui v1alpha1.UpgradeInsights) ([]*types.Insight, error) { + logger := log.FromContext(ctx) + var result []types.InsightSummary + + out, err := client.ListInsights(ctx, &eks.ListInsightsInput{ + ClusterName: lo.ToPtr(in.clusterName), + }) + if err != nil { + return nil, err + } + + result = out.Insights + nextToken := out.NextToken + for out.NextToken != nil { + out, err = client.ListInsights(ctx, &eks.ListInsightsInput{ + ClusterName: lo.ToPtr(in.clusterName), + NextToken: nextToken, + }) + if err != nil { + return nil, err + } + + nextToken = out.NextToken + } + + return algorithms.Filter( + algorithms.Map(result, func(insight types.InsightSummary) *types.Insight { + output, err := client.DescribeInsight(ctx, &eks.DescribeInsightInput{ + ClusterName: lo.ToPtr(in.clusterName), + Id: insight.Id, + }) + // If there is an error getting the details of an insight just ignore. + // It will be picked up during the next reconcile. + if err != nil { + logger.Error(err, "could not describe insight", "clusterName", in.clusterName, "id", insight.Id) + return nil + } + + return output.Insight + }), func(insight *types.Insight) bool { + return insight != nil + }), nil +} + +func (in *EKSCloudProvider) fromInsightStatus(status *types.InsightStatus) *console.UpgradeInsightStatus { + if status == nil { + return nil + } + + switch status.Status { + case types.InsightStatusValuePassing: + return lo.ToPtr(console.UpgradeInsightStatusPassing) + case types.InsightStatusValueError: + case types.InsightStatusValueWarning: + return lo.ToPtr(console.UpgradeInsightStatusFailed) + case types.InsightStatusValueUnknown: + return lo.ToPtr(console.UpgradeInsightStatusUnknown) + } + + return nil +} + +func (in *EKSCloudProvider) toInsightDetails(insight *types.Insight) []*console.UpgradeInsightDetailAttributes { + if insight.CategorySpecificSummary == nil { + return nil + } + + result := make([]*console.UpgradeInsightDetailAttributes, 0) + for _, r := range insight.CategorySpecificSummary.DeprecationDetails { + result = append(result, &console.UpgradeInsightDetailAttributes{ + Used: r.Usage, + Replacement: r.ReplacedWith, + ReplacedIn: r.StartServingReplacementVersion, + RemovedIn: r.StopServingVersion, + }) + } + + return result +} + +func (in *EKSCloudProvider) config(ctx context.Context, ui v1alpha1.UpgradeInsights) (aws.Config, error) { + // If credentials are not provided in the request, then use default credentials. + if ui.Spec.Credentials == nil || ui.Spec.Credentials.AWS == nil { + return awsconfig.LoadDefaultConfig(ctx) + } + + // Otherwise use provided credentials. + credentials := ui.Spec.Credentials.AWS + secretAccessKey, err := in.handleSecretAccessKeyRef(ctx, ui.Spec.Credentials.AWS.SecretAccessKeyRef, ui.Namespace) + if err != nil { + return aws.Config{}, err + } + + config, err := awsconfig.LoadDefaultConfig(ctx) + if err != nil { + return aws.Config{}, err + } + + config.Region = credentials.Region + config.Credentials = awscredentials.NewStaticCredentialsProvider( + credentials.AccessKeyID, secretAccessKey, "") + + return config, nil +} + +func (in *EKSCloudProvider) handleSecretAccessKeyRef(ctx context.Context, ref corev1.SecretReference, namespace string) (string, error) { + secret := &corev1.Secret{} + + if err := in.kubeClient.Get( + ctx, + runtimeclient.ObjectKey{Name: ref.Name, Namespace: ref.Namespace}, + secret, + ); err != nil { + return "", err + } + + key := "secretAccessKey" + value, exists := secret.Data[key] + if !exists { + return "", fmt.Errorf("secret %s/%s does not contain key %s", namespace, ref.Name, key) + } + + return string(value), nil +} + +func (in *EKSCloudProvider) client(ctx context.Context, ui v1alpha1.UpgradeInsights) (*eks.Client, error) { + config, err := in.config(ctx, ui) + if err != nil { + return nil, err + } + + return eks.NewFromConfig(config), nil +} + +func newEKSCloudProvider(kubeClient runtimeclient.Client, clusterName string) CloudProvider { + return &EKSCloudProvider{ + kubeClient: kubeClient, + clusterName: clusterName, + } +} + +func NewCloudProvider(distro *console.ClusterDistro, kubeClient runtimeclient.Client, clusterName string) (CloudProvider, error) { + if distro == nil { + return nil, fmt.Errorf("distro cannot be nil") + } + + if *distro == console.ClusterDistroEks { + return newEKSCloudProvider(kubeClient, clusterName), nil + } + + return nil, fmt.Errorf("unsupported distro: %s", *distro) +} diff --git a/internal/controller/upgradeinsights_controller.go b/internal/controller/upgradeinsights_controller.go new file mode 100644 index 00000000..7c2fa345 --- /dev/null +++ b/internal/controller/upgradeinsights_controller.go @@ -0,0 +1,137 @@ +package controller + +import ( + "context" + "time" + + console "github.com/pluralsh/console/go/client" + "github.com/samber/lo" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + k8sClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/pluralsh/deployment-operator/api/v1alpha1" + "github.com/pluralsh/deployment-operator/internal/utils" + "github.com/pluralsh/deployment-operator/pkg/client" +) + +// UpgradeInsightsController reconciler a v1alpha1.UpgradeInsights resource. +// Implements [reconcile.Reconciler] interface. +type UpgradeInsightsController struct { + k8sClient.Client + + Scheme *runtime.Scheme + ConsoleClient client.Client + + myCluster *console.MyCluster_MyCluster_ +} + +func (in *UpgradeInsightsController) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, reterr error) { + logger := log.FromContext(ctx) + + // Read resource from Kubernetes cluster. + ui := &v1alpha1.UpgradeInsights{} + if err := in.Get(ctx, req.NamespacedName, ui); err != nil { + logger.Error(err, "unable to fetch upgrade insights") + return ctrl.Result{}, k8sClient.IgnoreNotFound(err) + } + + if err := in.initCluster(); err != nil { + return ctrl.Result{}, err + } + + logger.Info("reconciling UpgradeInsights", "namespace", ui.Namespace, "name", ui.Name) + utils.MarkCondition(ui.SetCondition, v1alpha1.ReadyConditionType, metav1.ConditionFalse, v1alpha1.ReadyConditionReason, "") + + scope, err := NewDefaultScope(ctx, in.Client, ui) + if err != nil { + logger.Error(err, "failed to create scope") + utils.MarkCondition(ui.SetCondition, v1alpha1.SynchronizedConditionType, metav1.ConditionFalse, v1alpha1.ErrorConditionReason, err.Error()) + return ctrl.Result{}, err + } + + // Always patch object when exiting this function, so we can persist any object changes. + defer func() { + if err := scope.PatchObject(); err != nil && reterr == nil { + reterr = err + } + }() + + // Handle resource deletion + result, err := in.handleDelete(ctx, ui) + if result != nil { + return *result, err + } + + // Sync UpgradeInsights with the Console API + err = in.sync(ctx, ui) + if err != nil { + logger.Error(err, "unable to save upgrade insights") + utils.MarkCondition(ui.SetCondition, v1alpha1.SynchronizedConditionType, metav1.ConditionFalse, v1alpha1.ErrorConditionReason, err.Error()) + return ctrl.Result{}, err + } + + utils.MarkCondition(ui.SetCondition, v1alpha1.ReadyConditionType, metav1.ConditionTrue, v1alpha1.ReadyConditionReason, "") + utils.MarkCondition(ui.SetCondition, v1alpha1.SynchronizedConditionType, metav1.ConditionTrue, v1alpha1.SynchronizedConditionReason, time.Now().Format(time.RFC3339)) + + return requeue(ui.Spec.GetInterval(), jitter), reterr +} + +func (in *UpgradeInsightsController) handleDelete(ctx context.Context, ui *v1alpha1.UpgradeInsights) (*ctrl.Result, error) { + logger := log.FromContext(ctx) + + // If object is not being deleted + if ui.GetDeletionTimestamp().IsZero() { + // do nothing + return nil, nil + } + + // If object is being deleted + logger.Info("deleting UpgradeInsights", "namespace", ui.Namespace, "name", ui.Name) + return &ctrl.Result{}, nil +} + +func (in *UpgradeInsightsController) sync(ctx context.Context, ui *v1alpha1.UpgradeInsights) error { + cloudProvider, err := NewCloudProvider( + ui.Spec.GetDistro(in.myCluster.GetDistro()), + in.Client, + ui.Spec.GetClusterName(in.myCluster.GetName()), + ) + if err != nil { + return err + } + + attributes, err := cloudProvider.UpgradeInsights(ctx, *ui) + if err != nil { + return err + } + + _, err = in.ConsoleClient.SaveUpgradeInsights(lo.ToSlicePtr(attributes)) + return err +} + +func (in *UpgradeInsightsController) initCluster() error { + if in.myCluster != nil { + return nil + } + + myCluster, err := in.ConsoleClient.MyCluster() + if err != nil { + return err + } + + in.myCluster = myCluster.MyCluster + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (in *UpgradeInsightsController) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.UpgradeInsights{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Complete(in) +} diff --git a/internal/controller/virtualcluster_controller.go b/internal/controller/virtualcluster_controller.go index d1efb11f..0afd359c 100644 --- a/internal/controller/virtualcluster_controller.go +++ b/internal/controller/virtualcluster_controller.go @@ -66,7 +66,7 @@ func (in *VirtualClusterController) Reconcile(ctx context.Context, req reconcile scope, err := NewDefaultScope(ctx, in.Client, vCluster) if err != nil { logger.Error(err, "failed to create scope") - utils.MarkCondition(vCluster.SetCondition, v1alpha1.SynchronizedConditionType, metav1.ConditionFalse, v1alpha1.ErrorConditionReason, "%v", err) + utils.MarkCondition(vCluster.SetCondition, v1alpha1.SynchronizedConditionType, metav1.ConditionFalse, v1alpha1.ErrorConditionReason, err.Error()) return ctrl.Result{}, err } @@ -87,7 +87,7 @@ func (in *VirtualClusterController) Reconcile(ctx context.Context, req reconcile changed, sha, err := vCluster.Diff(utils.HashObject) if err != nil { logger.Error(err, "unable to calculate virtual cluster SHA") - utils.MarkCondition(vCluster.SetCondition, v1alpha1.SynchronizedConditionType, metav1.ConditionFalse, v1alpha1.ErrorConditionReason, "%v", err) + utils.MarkCondition(vCluster.SetCondition, v1alpha1.SynchronizedConditionType, metav1.ConditionFalse, v1alpha1.ErrorConditionReason, err.Error()) return ctrl.Result{}, err } @@ -95,7 +95,7 @@ func (in *VirtualClusterController) Reconcile(ctx context.Context, req reconcile apiVCluster, err := in.sync(ctx, vCluster, changed) if err != nil { logger.Error(err, "unable to create or update virtual cluster") - utils.MarkCondition(vCluster.SetCondition, v1alpha1.SynchronizedConditionType, metav1.ConditionFalse, v1alpha1.ErrorConditionReason, "%v", err) + utils.MarkCondition(vCluster.SetCondition, v1alpha1.SynchronizedConditionType, metav1.ConditionFalse, v1alpha1.ErrorConditionReason, err.Error()) return ctrl.Result{}, err } @@ -186,7 +186,7 @@ func (in *VirtualClusterController) sync(ctx context.Context, vCluster *v1alpha1 if in.shouldDeployVCluster(vCluster, changed) { err = in.deployVCluster(ctx, vCluster) if err != nil { - utils.MarkCondition(vCluster.SetCondition, v1alpha1.VirtualClusterConditionType, metav1.ConditionFalse, v1alpha1.ErrorConditionReason, "%v", err) + utils.MarkCondition(vCluster.SetCondition, v1alpha1.VirtualClusterConditionType, metav1.ConditionFalse, v1alpha1.ErrorConditionReason, err.Error()) return nil, err } @@ -196,7 +196,7 @@ func (in *VirtualClusterController) sync(ctx context.Context, vCluster *v1alpha1 if in.shouldDeployAgent(vCluster, changed) { err = in.deployAgent(ctx, vCluster, *createdVCluster.DeployToken) if err != nil { - utils.MarkCondition(vCluster.SetCondition, v1alpha1.AgentConditionType, metav1.ConditionFalse, v1alpha1.ErrorConditionReason, "%v", err) + utils.MarkCondition(vCluster.SetCondition, v1alpha1.AgentConditionType, metav1.ConditionFalse, v1alpha1.ErrorConditionReason, err.Error()) return nil, err } diff --git a/internal/utils/kubernetes.go b/internal/utils/kubernetes.go index bd985470..88eab6ce 100644 --- a/internal/utils/kubernetes.go +++ b/internal/utils/kubernetes.go @@ -56,12 +56,12 @@ func AsName(val string) string { return strings.ReplaceAll(val, " ", "-") } -func MarkCondition(set func(condition metav1.Condition), conditionType v1alpha1.ConditionType, conditionStatus metav1.ConditionStatus, conditionReason v1alpha1.ConditionReason, message string, messageArgs ...interface{}) { +func MarkCondition(set func(condition metav1.Condition), conditionType v1alpha1.ConditionType, conditionStatus metav1.ConditionStatus, conditionReason v1alpha1.ConditionReason, message string) { set(metav1.Condition{ Type: conditionType.String(), Status: conditionStatus, Reason: conditionReason.String(), - Message: fmt.Sprintf(message, messageArgs...), + Message: message, }) } diff --git a/pkg/client/console.go b/pkg/client/console.go index c405c91c..07aa7685 100644 --- a/pkg/client/console.go +++ b/pkg/client/console.go @@ -68,4 +68,5 @@ type Client interface { ListClusterStackRuns(after *string, first *int64) (*console.ListClusterStacks_ClusterStackRuns, error) GetUser(email string) (*console.UserFragment, error) GetGroup(name string) (*console.GroupFragment, error) + SaveUpgradeInsights(attributes []*console.UpgradeInsightAttributes) (*console.SaveUpgradeInsights, error) } diff --git a/pkg/client/upgradeinsights.go b/pkg/client/upgradeinsights.go new file mode 100644 index 00000000..43db796e --- /dev/null +++ b/pkg/client/upgradeinsights.go @@ -0,0 +1,9 @@ +package client + +import ( + console "github.com/pluralsh/console/go/client" +) + +func (c *client) SaveUpgradeInsights(attributes []*console.UpgradeInsightAttributes) (*console.SaveUpgradeInsights, error) { + return c.consoleClient.SaveUpgradeInsights(c.ctx, attributes) +} diff --git a/pkg/test/mocks/Client_mock.go b/pkg/test/mocks/Client_mock.go index 5cb9b45f..70ebacdd 100644 --- a/pkg/test/mocks/Client_mock.go +++ b/pkg/test/mocks/Client_mock.go @@ -1499,6 +1499,64 @@ func (_c *ClientMock_SaveClusterBackup_Call) RunAndReturn(run func(client.Backup return _c } +// SaveUpgradeInsights provides a mock function with given fields: attributes +func (_m *ClientMock) SaveUpgradeInsights(attributes []*client.UpgradeInsightAttributes) (*client.SaveUpgradeInsights, error) { + ret := _m.Called(attributes) + + if len(ret) == 0 { + panic("no return value specified for SaveUpgradeInsights") + } + + var r0 *client.SaveUpgradeInsights + var r1 error + if rf, ok := ret.Get(0).(func([]*client.UpgradeInsightAttributes) (*client.SaveUpgradeInsights, error)); ok { + return rf(attributes) + } + if rf, ok := ret.Get(0).(func([]*client.UpgradeInsightAttributes) *client.SaveUpgradeInsights); ok { + r0 = rf(attributes) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.SaveUpgradeInsights) + } + } + + if rf, ok := ret.Get(1).(func([]*client.UpgradeInsightAttributes) error); ok { + r1 = rf(attributes) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ClientMock_SaveUpgradeInsights_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveUpgradeInsights' +type ClientMock_SaveUpgradeInsights_Call struct { + *mock.Call +} + +// SaveUpgradeInsights is a helper method to define mock.On call +// - attributes []*client.UpgradeInsightAttributes +func (_e *ClientMock_Expecter) SaveUpgradeInsights(attributes interface{}) *ClientMock_SaveUpgradeInsights_Call { + return &ClientMock_SaveUpgradeInsights_Call{Call: _e.mock.On("SaveUpgradeInsights", attributes)} +} + +func (_c *ClientMock_SaveUpgradeInsights_Call) Run(run func(attributes []*client.UpgradeInsightAttributes)) *ClientMock_SaveUpgradeInsights_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]*client.UpgradeInsightAttributes)) + }) + return _c +} + +func (_c *ClientMock_SaveUpgradeInsights_Call) Return(_a0 *client.SaveUpgradeInsights, _a1 error) *ClientMock_SaveUpgradeInsights_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ClientMock_SaveUpgradeInsights_Call) RunAndReturn(run func([]*client.UpgradeInsightAttributes) (*client.SaveUpgradeInsights, error)) *ClientMock_SaveUpgradeInsights_Call { + _c.Call.Return(run) + return _c +} + // UpdateClusterRestore provides a mock function with given fields: id, attrs func (_m *ClientMock) UpdateClusterRestore(id string, attrs client.RestoreAttributes) (*client.ClusterRestoreFragment, error) { ret := _m.Called(id, attrs)