diff --git a/api/v1alpha1/upgradeinsights_types.go b/api/v1alpha1/upgradeinsights_types.go new file mode 100644 index 00000000..31e1d017 --- /dev/null +++ b/api/v1alpha1/upgradeinsights_types.go @@ -0,0 +1,117 @@ +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 ... + // +kubebuilder:validation:Required + Region string `json:"region"` + + // AccessKeyID ... + // +kubebuilder:validation:Required + AccessKeyID string `json:"accessKeyID"` + + // SecretAccessKeyRef ... + // +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..f39be55b --- /dev/null +++ b/config/crd/bases/deployments.plural.sh_upgradeinsights.yaml @@ -0,0 +1,186 @@ +--- +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 ... + type: string + region: + description: Region ... + type: string + secretAccessKeyRef: + description: SecretAccessKeyRef ... + 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/go.mod b/go.mod index 2066d8b6..dae364ad 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.19.1 + github.com/aws/aws-sdk-go-v2/credentials v1.13.43 + 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,15 @@ 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.13.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.3.45 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 // 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 +213,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..08eeed09 100644 --- a/go.sum +++ b/go.sum @@ -89,6 +89,36 @@ 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.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM= +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.19.1 h1:oe3vqcGftyk40icfLymhhhNysAwk0NfiwkDi2GTPMXs= +github.com/aws/aws-sdk-go-v2/config v1.19.1/go.mod h1:ZwDUgFnQgsazQTnWfeLWk5GjeqTQTL8lMkoE1UXzxdE= +github.com/aws/aws-sdk-go-v2/credentials v1.13.43 h1:LU8vo40zBlo3R7bAvBVy/ku4nxGEyZe9N8MqAeFTzF8= +github.com/aws/aws-sdk-go-v2/credentials v1.13.43/go.mod h1:zWJBz1Yf1ZtX5NGax9ZdNjhhI4rgjfgsyk6vTY1yfVg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 h1:PIktER+hwIG286DqXyvVENjgLTAwGgoeriLDD5C+YlQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13/go.mod h1:f/Ib/qYjhV2/qdsf79H3QP/eRE4AkVyEf6sk7XfZ1tg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43/go.mod h1:auo+PiyLl0n1l8A0e8RIeR8tOzYPfZZH/JNlrJ8igTQ= +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.4.37/go.mod h1:Qe+2KtKml+FEsQF/DHmDV+xjtche/hwoF75EG4UlHW8= +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.3.45 h1:hze8YsjSh8Wl1rYa1CJpRmXP21BvOBuc76YhW0HsuQ4= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45/go.mod h1:lD5M20o09/LCuQ2mE62Mb/iSdSlCNuj6H5ci7tW7OsE= +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/presigned-url v1.9.37 h1:WWZA/I2K4ptBS1kg0kV1JbBtG/umed0vwHRrmcr9z7k= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck= +github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 h1:JuPGc7IkOP4AaqcZSIcyqLpFSqBWK32rM9+a1g6u73k= +github.com/aws/aws-sdk-go-v2/service/sso v1.15.2/go.mod h1:gsL4keucRCgW+xA85ALBpRFfdSLH4kHOVSnLMSuBECo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 h1:HFiiRkf1SdaAmV3/BHOFZ9DjFynPHj8G/UIO1lQS+fk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3/go.mod h1:a7bHA82fyUXOm+ZSWKU6PIoBxrjSprdLoM8xPYvzYVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 h1:0BkLfgeDjfZnZ+MhB3ONb01u9pwFYTCZVhlsSSBvlbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.23.2/go.mod h1:Eows6e1uQEsc4ZaHANmsPRzAKcVDrcmjjWiih2+HUUQ= +github.com/aws/smithy-go v1.15.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +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 +475,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 +674,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..18f43541 --- /dev/null +++ b/internal/controller/upgradeinsights_cloudprovider.go @@ -0,0 +1,181 @@ +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" + + "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.InsightSummary) 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), + RefreshedAt: refreshedAt, + TransitionedAt: transitionedAt, + } + }), nil +} + +func (in *EKSCloudProvider) listInsights(ctx context.Context, client *eks.Client, ui v1alpha1.UpgradeInsights) ([]types.InsightSummary, error) { + var result []types.InsightSummary + + out, err := client.ListInsights(ctx, &eks.ListInsightsInput{ + ClusterName: lo.ToPtr(ui.Spec.GetClusterName(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(ui.Spec.GetClusterName(in.clusterName)), + NextToken: nextToken, + }) + if err != nil { + return nil, err + } + + nextToken = out.NextToken + } + + return result, 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) 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") + } + + switch *distro { + case 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..346533fb --- /dev/null +++ b/internal/controller/upgradeinsights_controller.go @@ -0,0 +1,133 @@ +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, 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)