diff --git a/api/v1alpha1/managedcluster_types.go b/api/v1alpha1/managedcluster_types.go index 3f5e40bc1..03cd0118a 100644 --- a/api/v1alpha1/managedcluster_types.go +++ b/api/v1alpha1/managedcluster_types.go @@ -92,6 +92,8 @@ type ManagedClusterSpec struct { // ManagedClusterStatus defines the observed state of ManagedCluster type ManagedClusterStatus struct { + // Services contains details for the state of services. + Services []ServiceStatus `json:"services,omitempty"` // Currently compatible exact Kubernetes version of the cluster. Being set only if // provided by the corresponding ClusterTemplate. KubernetesVersion string `json:"k8sVersion,omitempty"` diff --git a/api/v1alpha1/multiclusterservice_types.go b/api/v1alpha1/multiclusterservice_types.go index 5e3633447..c6bf87f59 100644 --- a/api/v1alpha1/multiclusterservice_types.go +++ b/api/v1alpha1/multiclusterservice_types.go @@ -24,6 +24,14 @@ const ( MultiClusterServiceFinalizer = "hmc.mirantis.com/multicluster-service" // MultiClusterServiceKind is the string representation of a MultiClusterServiceKind. MultiClusterServiceKind = "MultiClusterService" + + // SveltosProfileReadyCondition indicates if the Sveltos Profile is ready. + SveltosProfileReadyCondition = "SveltosProfileReady" + // SveltosClusterProfileReadyCondition indicates if the Sveltos ClusterProfile is ready. + SveltosClusterProfileReadyCondition = "SveltosClusterProfileReady" + // SveltosHelmReleaseReadyCondition indicates if the HelmRelease + // managed by a Sveltos Profile/ClusterProfile is ready. + SveltosHelmReleaseReadyCondition = "SveltosHelmReleaseReady" ) // ServiceSpec represents a Service to be managed @@ -74,14 +82,24 @@ type MultiClusterServiceSpec struct { StopOnConflict bool `json:"stopOnConflict,omitempty"` } -// MultiClusterServiceStatus defines the observed state of MultiClusterService -// -// TODO(https://github.com/Mirantis/hmc/issues/460): -// If this status ends up being common with ManagedClusterStatus, -// then make a common status struct that can be shared by both. +// ServiceStatus contains details for the state of services. +type ServiceStatus struct { + // ClusterName is the name of the associated cluster. + ClusterName string `json:"clusterName"` + // ClusterNamespace is the namespace of the associated cluster. + ClusterNamespace string `json:"clusterNamespace,omitempty"` + // Conditions contains details for the current state of managed services. + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// MultiClusterServiceStatus defines the observed state of MultiClusterService. type MultiClusterServiceStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file + // Services contains details for the state of services. + Services []ServiceStatus `json:"services,omitempty"` + // Conditions contains details for the current state of the ManagedCluster + Conditions []metav1.Condition `json:"conditions,omitempty"` + // ObservedGeneration is the last observed generation. + ObservedGeneration int64 `json:"observedGeneration,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index c0d4b348d..59bcaa4e2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -531,6 +531,13 @@ func (in *ManagedClusterSpec) DeepCopy() *ManagedClusterSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagedClusterStatus) DeepCopyInto(out *ManagedClusterStatus) { *out = *in + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = make([]ServiceStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]metav1.Condition, len(*in)) @@ -692,7 +699,7 @@ func (in *MultiClusterService) DeepCopyInto(out *MultiClusterService) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MultiClusterService. @@ -771,6 +778,20 @@ func (in *MultiClusterServiceSpec) DeepCopy() *MultiClusterServiceSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MultiClusterServiceStatus) DeepCopyInto(out *MultiClusterServiceStatus) { *out = *in + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = make([]ServiceStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MultiClusterServiceStatus. @@ -1072,6 +1093,28 @@ func (in *ServiceSpec) DeepCopy() *ServiceSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceStatus) DeepCopyInto(out *ServiceStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceStatus. +func (in *ServiceStatus) DeepCopy() *ServiceStatus { + if in == nil { + return nil + } + out := new(ServiceStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceTemplate) DeepCopyInto(out *ServiceTemplate) { *out = *in diff --git a/go.mod b/go.mod index 17653084c..26507c551 100644 --- a/go.mod +++ b/go.mod @@ -56,6 +56,7 @@ require ( github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/cyphar/filepath-securejoin v0.3.3 // indirect + github.com/dariubs/percent v1.0.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v27.3.1+incompatible // indirect @@ -72,8 +73,11 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fluxcd/pkg/apis/acl v0.3.0 // indirect github.com/fluxcd/pkg/apis/kustomize v1.6.1 // indirect + github.com/fluxcd/pkg/http/fetch v0.12.1 // indirect + github.com/fluxcd/pkg/tar v0.8.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/gdexlab/go-render v1.0.1 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -108,6 +112,7 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.10 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lib/pq v1.10.9 // indirect @@ -127,6 +132,7 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/opencontainers/go-digest/blake3 v0.0.0-20240426182413-22b78e47854a // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect @@ -151,6 +157,8 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect + github.com/zeebo/blake3 v0.2.4 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect go.opentelemetry.io/otel v1.30.0 // indirect go.opentelemetry.io/otel/metric v1.30.0 // indirect @@ -177,6 +185,7 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/apiserver v0.31.2 // indirect k8s.io/cli-runtime v0.31.2 // indirect + k8s.io/cluster-bootstrap v0.31.1 // indirect k8s.io/component-base v0.31.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094 // indirect diff --git a/go.sum b/go.sum index c33143914..f33a0d5b4 100644 --- a/go.sum +++ b/go.sum @@ -45,6 +45,8 @@ github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg= github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -79,6 +81,10 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coredns/caddy v1.1.1 h1:2eYKZT7i6yxIfGP3qLJoJ7HAsDJqYB+X68g4NYjSrE0= +github.com/coredns/caddy v1.1.1/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= +github.com/coredns/corefile-migration v1.0.23 h1:Fp4FETmk8sT/IRgnKX2xstC2dL7+QdcU+BL5AYIN3Jw= +github.com/coredns/corefile-migration v1.0.23/go.mod h1:8HyMhuyzx9RLZp8cRc9Uf3ECpEAafHOFxQWUPqktMQI= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -87,6 +93,8 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.3.3 h1:lofZkCEVFIBe0KcdQOzFs8Soy9oaHOWl4gGtPI+gCFc= github.com/cyphar/filepath-securejoin v0.3.3/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM= +github.com/dariubs/percent v1.0.0 h1:fY8q40FRYaCiFZ0gTOa73Cmp21hS32w+tSSmqbGnUzc= +github.com/dariubs/percent v1.0.0/go.mod h1:NDZpkezJ8QqyIW/510MywB5T2KdC8v/0oTlEoPcMsRM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -133,8 +141,14 @@ github.com/fluxcd/pkg/apis/kustomize v1.6.1 h1:22FJc69Mq4i8aCxnKPlddHhSMyI4UPkQk github.com/fluxcd/pkg/apis/kustomize v1.6.1/go.mod h1:5dvQ4IZwz0hMGmuj8tTWGtarsuxW0rWsxJOwC6i+0V8= github.com/fluxcd/pkg/apis/meta v1.6.1 h1:maLhcRJ3P/70ArLCY/LF/YovkxXbX+6sTWZwZQBeNq0= github.com/fluxcd/pkg/apis/meta v1.6.1/go.mod h1:YndB/gxgGZmKfqpAfFxyCDNFJFP0ikpeJzs66jwq280= +github.com/fluxcd/pkg/http/fetch v0.12.1 h1:Iap/cdKols3fW39/MyTGqNXHglaA1FJsWtFgYG2hbCQ= +github.com/fluxcd/pkg/http/fetch v0.12.1/go.mod h1:t3JL+uqJ46Wm0CwVRn6Pf/3kOqh45tMoR0pMxLhextQ= github.com/fluxcd/pkg/runtime v0.49.1 h1:Xyruu1VvkaKZaAhm/32tHJnHab9aU3HzZCf+w6Xoq2A= github.com/fluxcd/pkg/runtime v0.49.1/go.mod h1:ieDaIEcxzVj77Nw64q4Vd3ZGYdLqpnXOr+GX+XwqTS4= +github.com/fluxcd/pkg/tar v0.8.1 h1:K9RWV+E/+Qbz6Mzcg+S9DkVvZrWwJq4957Kqms183RQ= +github.com/fluxcd/pkg/tar v0.8.1/go.mod h1:vuGrnXQPcdi3M4DoVtwvAyvLnSeFgXRJckTGYuZOy2Q= +github.com/fluxcd/pkg/testserver v0.7.0 h1:kNVAn+3bAF2rfR9cT6SxzgEz2o84i+o7zKY3XRKTXmk= +github.com/fluxcd/pkg/testserver v0.7.0/go.mod h1:Ih5IK3Y5G3+a6c77BTqFkdPDCY1Yj1A1W5cXQqkCs9s= github.com/fluxcd/source-controller/api v1.4.1 h1:zV01D7xzHOXWbYXr36lXHWWYS7POARsjLt61Nbh3kVY= github.com/fluxcd/source-controller/api v1.4.1/go.mod h1:gSjg57T+IG66SsBR0aquv+DFrm4YyBNpKIJVDnu3Ya8= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= @@ -145,6 +159,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gdexlab/go-render v1.0.1 h1:rxqB3vo5s4n1kF0ySmoNeSPRYkEsyHgln4jFIQY7v0U= +github.com/gdexlab/go-render v1.0.1/go.mod h1:wRi5nW2qfjiGj4mPukH4UV0IknS1cHD4VgFTmJX5JzM= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= @@ -188,6 +204,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI= +github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -251,6 +269,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -320,6 +340,8 @@ github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/opencontainers/go-digest v1.0.1-0.20231025023718-d50d2fec9c98 h1:H55sU3giNgBkIvmAo0vI/AAFwVTwfWsf6MN3+9H6U8o= github.com/opencontainers/go-digest v1.0.1-0.20231025023718-d50d2fec9c98/go.mod h1:RqnyioA3pIEZMkSbOIcrw32YSgETfn/VrLuEikEdPNU= +github.com/opencontainers/go-digest/blake3 v0.0.0-20240426182413-22b78e47854a h1:xwooQrLddjfeKhucuLS4ElD3TtuuRwF8QWC9eHrnbxY= +github.com/opencontainers/go-digest/blake3 v0.0.0-20240426182413-22b78e47854a/go.mod h1:kqQaIc6bZstKgnGpL7GD5dWoLKbA6mH1Y9ULjGImBnM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= @@ -391,6 +413,8 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace h1:9PNP1jnUjRhfmGMlkXHjYPishpcw4jpSt/V/xYY3FMA= github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -422,6 +446,14 @@ github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEAB github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/exporters/autoexport v0.46.1 h1:ysCfPZB9AjUlMa1UHYup3c9dAOCMQX/6sxSfPBUoxHw= @@ -500,6 +532,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -565,6 +598,8 @@ k8s.io/cli-runtime v0.31.2 h1:7FQt4C4Xnqx8V1GJqymInK0FFsoC+fAZtbLqgXYVOLQ= k8s.io/cli-runtime v0.31.2/go.mod h1:XROyicf+G7rQ6FQJMbeDV9jqxzkWXTYD6Uxd15noe0Q= k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc= k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs= +k8s.io/cluster-bootstrap v0.31.1 h1:lS5aJi2r6WEKnjO5UhbYsz8e3xmEfoF4Hiob/gnB/Nk= +k8s.io/cluster-bootstrap v0.31.1/go.mod h1:dxroRr4eQ0ekxis/kzGa1qODprQXAxQZrgDLfTk8Pug= k8s.io/component-base v0.31.2 h1:Z1J1LIaC0AV+nzcPRFqfK09af6bZ4D1nAOpWsy9owlA= k8s.io/component-base v0.31.2/go.mod h1:9PeyyFN/drHjtJZMCTkSpQJS3U9OXORnHQqMLDz0sUQ= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= @@ -577,6 +612,8 @@ k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go v1.2.6 h1:z8cmxQXBU8yZ4mkytWqXfo6tZcamPwjsuxYU81xJ8Lk= oras.land/oras-go v1.2.6/go.mod h1:OVPc1PegSEe/K8YiLfosrlqlqTN9PUyFvOw5Y9gwrT8= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/cluster-api v1.8.4 h1:jBKQH1H/HUdUFk8T6qDzIxZJfWw1F5ZP0ZpYQJDmTHs= sigs.k8s.io/cluster-api v1.8.4/go.mod h1:pXv5LqLxuIbhGIXykyNKiJh+KrLweSBajVHHitPLyoY= sigs.k8s.io/cluster-api-provider-azure v1.17.1 h1:f1sTGfv6hAN9WrxeawE4pQ2nRhEKb7AJjH6MhU/wAzg= diff --git a/internal/controller/managedcluster_controller.go b/internal/controller/managedcluster_controller.go index 3b8805317..e0bfefc27 100644 --- a/internal/controller/managedcluster_controller.go +++ b/internal/controller/managedcluster_controller.go @@ -28,6 +28,7 @@ import ( fluxmeta "github.com/fluxcd/pkg/apis/meta" fluxconditions "github.com/fluxcd/pkg/runtime/conditions" sourcev1 "github.com/fluxcd/source-controller/api/v1" + sveltosv1beta1 "github.com/projectsveltos/addon-controller/api/v1beta1" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" corev1 "k8s.io/api/core/v1" @@ -369,14 +370,35 @@ func (r *ManagedClusterReconciler) Update(ctx context.Context, managedCluster *h } // updateServices reconciles services provided in ManagedCluster.Spec.Services. -// TODO(https://github.com/Mirantis/hmc/issues/361): Set status to ManagedCluster object at appropriate places. -func (r *ManagedClusterReconciler) updateServices(ctx context.Context, mc *hmc.ManagedCluster) (ctrl.Result, error) { +func (r *ManagedClusterReconciler) updateServices(ctx context.Context, mc *hmc.ManagedCluster) (_ ctrl.Result, err error) { + // servicesErr is handled separately from err because we do not want + // to set the condition of SveltosProfileReady type to "False" + // if there is an error while retrieving status for the services. + var servicesErr error + + defer func() { + condition := metav1.Condition{ + Reason: hmc.SucceededReason, + Status: metav1.ConditionTrue, + Type: hmc.SveltosProfileReadyCondition, + } + + if err != nil { + condition.Message = err.Error() + condition.Reason = hmc.FailedReason + condition.Status = metav1.ConditionFalse + } + + apimeta.SetStatusCondition(&mc.Status.Conditions, condition) + err = errors.Join(err, servicesErr) + }() + opts, err := helmChartOpts(ctx, r.Client, mc.Namespace, mc.Spec.Services) if err != nil { return ctrl.Result{}, err } - if _, err := sveltos.ReconcileProfile(ctx, r.Client, mc.Namespace, mc.Name, + if _, err = sveltos.ReconcileProfile(ctx, r.Client, mc.Namespace, mc.Name, sveltos.ReconcileProfileOpts{ OwnerReference: &metav1.OwnerReference{ APIVersion: hmc.GroupVersion.String(), @@ -397,12 +419,25 @@ func (r *ManagedClusterReconciler) updateServices(ctx context.Context, mc *hmc.M return ctrl.Result{}, fmt.Errorf("failed to reconcile Profile: %w", err) } - // We don't technically need to requeue here, but doing so because golint fails with: - // `(*ManagedClusterReconciler).updateServices` - result `res` is always `nil` (unparam) - // - // This will be automatically resolved once setting status is implemented (https://github.com/Mirantis/hmc/issues/361), - // as it is likely that some execution path in the function will have to return with a requeue to fetch latest status. - return ctrl.Result{RequeueAfter: DefaultRequeueInterval}, nil + // NOTE: + // We are returning nil in the return statements whenever servicesErr != nil + // because we don't want the error content in servicesErr to be assigned to err. + // The servicesErr var is joined with err in the defer func() so this function + // will ultimately return the error in servicesErr instead of nil. + profile := sveltosv1beta1.Profile{} + profileRef := client.ObjectKey{Name: mc.Name, Namespace: mc.Namespace} + if servicesErr = r.Get(ctx, profileRef, &profile); servicesErr != nil { + servicesErr = fmt.Errorf("failed to get Profile %s to fetch status from its associated ClusterSummary: %w", profileRef.String(), servicesErr) + return ctrl.Result{}, nil + } + + servicesStatus, servicesErr := updateServicesStatus(ctx, r.Client, profileRef, sveltosv1beta1.ProfileKind, profile.Status, mc.Status.Services) + if servicesErr != nil { + return ctrl.Result{}, nil + } + mc.Status.Services = servicesStatus + + return ctrl.Result{}, nil } func validateReleaseWithValues(ctx context.Context, actionConfig *action.Configuration, managedCluster *hmc.ManagedCluster, hcChart *chart.Chart) error { @@ -423,46 +458,19 @@ func validateReleaseWithValues(ctx context.Context, actionConfig *action.Configu return nil } +// updateStatus updates the status for the ManagedCluster object. func (r *ManagedClusterReconciler) updateStatus(ctx context.Context, managedCluster *hmc.ManagedCluster, template *hmc.ClusterTemplate) error { managedCluster.Status.ObservedGeneration = managedCluster.Generation - warnings := "" - errs := "" - for _, condition := range managedCluster.Status.Conditions { - if condition.Type == hmc.ReadyCondition { - continue - } - if condition.Status == metav1.ConditionUnknown { - warnings += condition.Message + ". " - } - if condition.Status == metav1.ConditionFalse { - errs += condition.Message + ". " - } - } - condition := metav1.Condition{ - Type: hmc.ReadyCondition, - Status: metav1.ConditionTrue, - Reason: hmc.SucceededReason, - Message: "ManagedCluster is ready", - } - if warnings != "" { - condition.Status = metav1.ConditionUnknown - condition.Reason = hmc.ProgressingReason - condition.Message = warnings - } - if errs != "" { - condition.Status = metav1.ConditionFalse - condition.Reason = hmc.FailedReason - condition.Message = errs - } - apimeta.SetStatusCondition(managedCluster.GetConditions(), condition) + managedCluster.Status.Conditions = updateStatusConditions(managedCluster.Status.Conditions, "ManagedCluster is ready") - err := r.setAvailableUpgrades(ctx, managedCluster, template) - if err != nil { + if err := r.setAvailableUpgrades(ctx, managedCluster, template); err != nil { return errors.New("failed to set available upgrades") } + if err := r.Status().Update(ctx, managedCluster); err != nil { return fmt.Errorf("failed to update status for managedCluster %s/%s: %w", managedCluster.Namespace, managedCluster.Name, err) } + return nil } @@ -1092,5 +1100,12 @@ func (r *ManagedClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { GenericFunc: func(event.GenericEvent) bool { return false }, }), ). + Watches(&sveltosv1beta1.ClusterSummary{}, + handler.EnqueueRequestsFromMapFunc(requeueSveltosProfileForClusterSummary), + builder.WithPredicates(predicate.Funcs{ + DeleteFunc: func(event.DeleteEvent) bool { return false }, + GenericFunc: func(event.GenericEvent) bool { return false }, + }), + ). Complete(r) } diff --git a/internal/controller/multiclusterservice_controller.go b/internal/controller/multiclusterservice_controller.go index 28ba554d0..c18e244c5 100644 --- a/internal/controller/multiclusterservice_controller.go +++ b/internal/controller/multiclusterservice_controller.go @@ -16,14 +16,25 @@ package controller import ( "context" + "errors" "fmt" + "slices" sourcev1 "github.com/fluxcd/source-controller/api/v1" + sveltosv1beta1 "github.com/projectsveltos/addon-controller/api/v1beta1" + sveltoscontrollers "github.com/projectsveltos/addon-controller/controllers" + libsveltosv1beta1 "github.com/projectsveltos/libsveltos/api/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" hmc "github.com/Mirantis/hmc/api/v1alpha1" "github.com/Mirantis/hmc/internal/sveltos" @@ -40,8 +51,8 @@ func (r *MultiClusterServiceReconciler) Reconcile(ctx context.Context, req ctrl. l := ctrl.LoggerFrom(ctx) l.Info("Reconciling MultiClusterService") - mcsvc := &hmc.MultiClusterService{} - err := r.Get(ctx, req.NamespacedName, mcsvc) + mcs := &hmc.MultiClusterService{} + err := r.Get(ctx, req.NamespacedName, mcs) if apierrors.IsNotFound(err) { l.Info("MultiClusterService not found, ignoring since object must be deleted") return ctrl.Result{}, nil @@ -51,41 +62,85 @@ func (r *MultiClusterServiceReconciler) Reconcile(ctx context.Context, req ctrl. return ctrl.Result{}, err } - if !mcsvc.DeletionTimestamp.IsZero() { + if !mcs.DeletionTimestamp.IsZero() { l.Info("Deleting MultiClusterService") - return r.reconcileDelete(ctx, mcsvc) + return r.reconcileDelete(ctx, mcs) } - if controllerutil.AddFinalizer(mcsvc, hmc.MultiClusterServiceFinalizer) { - if err := r.Client.Update(ctx, mcsvc); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to update MultiClusterService %s with finalizer %s: %w", mcsvc.Name, hmc.MultiClusterServiceFinalizer, err) + return r.reconcileUpdate(ctx, mcs) +} + +func (r *MultiClusterServiceReconciler) reconcileUpdate(ctx context.Context, mcs *hmc.MultiClusterService) (_ ctrl.Result, err error) { + // servicesErr is handled separately from err because we do not want + // to set the condition of SveltosClusterProfileReady type to "False" + // if there is an error while retrieving status for the services. + var servicesErr error + + defer func() { + condition := metav1.Condition{ + Reason: hmc.SucceededReason, + Status: metav1.ConditionTrue, + Type: hmc.SveltosClusterProfileReadyCondition, } - return ctrl.Result{}, nil + + if err != nil { + condition.Message = err.Error() + condition.Reason = hmc.FailedReason + condition.Status = metav1.ConditionFalse + } + + apimeta.SetStatusCondition(&mcs.Status.Conditions, condition) + err = errors.Join(err, servicesErr, r.updateStatus(ctx, mcs)) + }() + + if controllerutil.AddFinalizer(mcs, hmc.MultiClusterServiceFinalizer) { + if err := r.Client.Update(ctx, mcs); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update MultiClusterService %s with finalizer %s: %w", mcs.Name, hmc.MultiClusterServiceFinalizer, err) + } + return ctrl.Result{Requeue: true}, nil } // By using DefaultSystemNamespace we are enforcing that MultiClusterService // may only use ServiceTemplates that are present in the hmc-system namespace. - opts, err := helmChartOpts(ctx, r.Client, utils.DefaultSystemNamespace, mcsvc.Spec.Services) + opts, err := helmChartOpts(ctx, r.Client, utils.DefaultSystemNamespace, mcs.Spec.Services) if err != nil { return ctrl.Result{}, err } - if _, err := sveltos.ReconcileClusterProfile(ctx, r.Client, mcsvc.Name, + if _, err := sveltos.ReconcileClusterProfile(ctx, r.Client, mcs.Name, sveltos.ReconcileProfileOpts{ OwnerReference: &metav1.OwnerReference{ APIVersion: hmc.GroupVersion.String(), Kind: hmc.MultiClusterServiceKind, - Name: mcsvc.Name, - UID: mcsvc.UID, + Name: mcs.Name, + UID: mcs.UID, }, - LabelSelector: mcsvc.Spec.ClusterSelector, + LabelSelector: mcs.Spec.ClusterSelector, HelmChartOpts: opts, - Priority: mcsvc.Spec.ServicesPriority, - StopOnConflict: mcsvc.Spec.StopOnConflict, + Priority: mcs.Spec.ServicesPriority, + StopOnConflict: mcs.Spec.StopOnConflict, }); err != nil { return ctrl.Result{}, fmt.Errorf("failed to reconcile ClusterProfile: %w", err) } + // NOTE: + // We are returning nil in the return statements whenever servicesErr != nil + // because we don't want the error content in servicesErr to be assigned to err. + // The servicesErr var is joined with err in the defer func() so this function + // will ultimately return the error in servicesErr instead of nil. + profile := sveltosv1beta1.ClusterProfile{} + profileRef := client.ObjectKey{Name: mcs.Name} + if servicesErr := r.Get(ctx, profileRef, &profile); servicesErr != nil { + servicesErr = fmt.Errorf("failed to get ClusterProfile %s to fetch status from its associated ClusterSummary: %w", profileRef.String(), servicesErr) + return ctrl.Result{}, nil + } + + servicesStatus, servicesErr := updateServicesStatus(ctx, r.Client, profileRef, sveltosv1beta1.ClusterProfileKind, profile.Status, mcs.Status.Services) + if servicesErr != nil { + return ctrl.Result{}, nil + } + mcs.Status.Services = servicesStatus + return ctrl.Result{}, nil } @@ -177,6 +232,90 @@ func helmChartOpts(ctx context.Context, c client.Client, namespace string, servi return opts, nil } +// updateStatus updates the status for the MultiClusterService object. +func (r *MultiClusterServiceReconciler) updateStatus(ctx context.Context, mcs *hmc.MultiClusterService) error { + mcs.Status.ObservedGeneration = mcs.Generation + mcs.Status.Conditions = updateStatusConditions(mcs.Status.Conditions, "MultiClusterService is ready") + + if err := r.Status().Update(ctx, mcs); err != nil { + return fmt.Errorf("failed to update status for MultiClusterService %s/%s: %w", mcs.Namespace, mcs.Name, err) + } + + return nil +} + +// updateStatusConditions evaluates all provided conditions and returns them +// after setting a new condition based on the status of the provided ones. +func updateStatusConditions(conditions []metav1.Condition, readyMsg string) []metav1.Condition { + warnings := "" + errs := "" + + for _, condition := range conditions { + if condition.Type == hmc.ReadyCondition { + continue + } + if condition.Status == metav1.ConditionUnknown { + warnings += condition.Message + ". " + } + if condition.Status == metav1.ConditionFalse { + errs += condition.Message + ". " + } + } + + condition := metav1.Condition{ + Type: hmc.ReadyCondition, + Status: metav1.ConditionTrue, + Reason: hmc.SucceededReason, + Message: readyMsg, + } + + if warnings != "" { + condition.Status = metav1.ConditionUnknown + condition.Reason = hmc.ProgressingReason + condition.Message = warnings + } + if errs != "" { + condition.Status = metav1.ConditionFalse + condition.Reason = hmc.FailedReason + condition.Message = errs + } + + apimeta.SetStatusCondition(&conditions, condition) + return conditions +} + +// updateServicesStatus updates the services deployment status. +func updateServicesStatus(ctx context.Context, c client.Client, profileRef types.NamespacedName, profileKind string, profileStatus sveltosv1beta1.Status, servicesStatus []hmc.ServiceStatus) ([]hmc.ServiceStatus, error) { + for _, obj := range profileStatus.MatchingClusterRefs { + isSveltosCluster := obj.APIVersion == libsveltosv1beta1.GroupVersion.String() + summaryName := sveltoscontrollers.GetClusterSummaryName(profileKind, profileRef.Name, obj.Name, isSveltosCluster) + + summary := sveltosv1beta1.ClusterSummary{} + summaryRef := client.ObjectKey{Name: summaryName, Namespace: obj.Namespace} + if err := c.Get(ctx, summaryRef, &summary); err != nil { + return nil, fmt.Errorf("failed to get ClusterSummary %s to fetch status: %w", summaryRef.String(), err) + } + + idx := slices.IndexFunc(servicesStatus, func(o hmc.ServiceStatus) bool { + return obj.Name == o.ClusterName && obj.Namespace == o.ClusterNamespace + }) + + if idx < 0 { + servicesStatus = append(servicesStatus, hmc.ServiceStatus{ + ClusterName: obj.Name, + ClusterNamespace: obj.Namespace, + }) + idx = len(servicesStatus) - 1 + } + + if err := sveltos.SetStatusConditions(&summary, &servicesStatus[idx].Conditions); err != nil { + return nil, err + } + } + + return servicesStatus, nil +} + func (r *MultiClusterServiceReconciler) reconcileDelete(ctx context.Context, mcsvc *hmc.MultiClusterService) (ctrl.Result, error) { if err := sveltos.DeleteClusterProfile(ctx, r.Client, mcsvc.Name); err != nil { return ctrl.Result{}, err @@ -191,9 +330,47 @@ func (r *MultiClusterServiceReconciler) reconcileDelete(ctx context.Context, mcs return ctrl.Result{}, nil } +// requeueSveltosProfileForClusterSummary asserts that the requested object has Sveltos ClusterSummary +// type, fetches its owner (a Sveltos Profile or ClusterProfile object), and requeues its reference. +// When used with ManagedClusterReconciler or MultiClusterServiceReconciler, this effectively +// requeues a ManagedCluster or MultiClusterService object as these are referenced by the same +// namespace/name as the Sveltos Profile or ClusterProfile object that they create respectively. +func requeueSveltosProfileForClusterSummary(ctx context.Context, obj client.Object) []ctrl.Request { + l := ctrl.LoggerFrom(ctx) + msg := "cannot queue request" + + cs, ok := obj.(*sveltosv1beta1.ClusterSummary) + if !ok { + l.Error(errors.New("request is not for a ClusterSummary object"), msg, "ClusterSummary.Name", obj.GetName(), "ClusterSummary.Namespace", obj.GetNamespace()) + return []ctrl.Request{} + } + + ownerRef, err := sveltosv1beta1.GetProfileOwnerReference(cs) + if err != nil { + l.Error(err, msg, "ClusterSummary.Name", obj.GetName(), "ClusterSummary.Namespace", obj.GetNamespace()) + return []ctrl.Request{} + } + + // The Profile/ClusterProfile object has the same name as its + // owner object which is either ManagedCluster or MultiClusterService. + req := client.ObjectKey{Name: ownerRef.Name} + if ownerRef.Kind == sveltosv1beta1.ProfileKind { + req.Namespace = obj.GetNamespace() + } + + return []ctrl.Request{{NamespacedName: req}} +} + // SetupWithManager sets up the controller with the Manager. func (r *MultiClusterServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&hmc.MultiClusterService{}). + For(&hmc.MultiClusterService{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Watches(&sveltosv1beta1.ClusterSummary{}, + handler.EnqueueRequestsFromMapFunc(requeueSveltosProfileForClusterSummary), + builder.WithPredicates(predicate.Funcs{ + DeleteFunc: func(event.DeleteEvent) bool { return false }, + GenericFunc: func(event.GenericEvent) bool { return false }, + }), + ). Complete(r) } diff --git a/internal/sveltos/profile.go b/internal/sveltos/profile.go index 2c3dc79d3..6e47811fb 100644 --- a/internal/sveltos/profile.go +++ b/internal/sveltos/profile.go @@ -16,6 +16,7 @@ package sveltos import ( "context" + "errors" "fmt" "math" "unsafe" @@ -23,6 +24,7 @@ import ( sveltosv1beta1 "github.com/projectsveltos/addon-controller/api/v1beta1" libsveltosv1beta1 "github.com/projectsveltos/libsveltos/api/v1beta1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -225,3 +227,56 @@ func priorityToTier(priority int32) (int32, error) { return 0, fmt.Errorf("invalid value %d, priority has to be between %d and %d", priority, mini, maxi) } + +// SetStatusConditions transforms status from Sveltos ClusterSummary +// object and sets it into the provided list of conditions. +func SetStatusConditions(summary *sveltosv1beta1.ClusterSummary, conditions *[]metav1.Condition) error { + if summary == nil { + return errors.New("nil summary provided") + } + + for _, x := range summary.Status.FeatureSummaries { + msg := "" + status := metav1.ConditionTrue + if x.FailureMessage != nil && *x.FailureMessage != "" { + msg = *x.FailureMessage + status = metav1.ConditionFalse + } + + apimeta.SetStatusCondition(conditions, metav1.Condition{ + Message: msg, + Reason: string(x.Status), + Status: status, + Type: string(x.FeatureID), + }) + } + + for _, x := range summary.Status.HelmReleaseSummaries { + msg := "Release " + x.ReleaseNamespace + "/" + x.ReleaseName + status := metav1.ConditionTrue + if x.ConflictMessage != "" { + msg += ": " + x.ConflictMessage + status = metav1.ConditionFalse + } + + apimeta.SetStatusCondition(conditions, metav1.Condition{ + Message: msg, + Reason: string(x.Status), + Status: status, + Type: HelmReleaseReadyConditionType(x.ReleaseNamespace, x.ReleaseName), + }) + } + + return nil +} + +// HelmReleaseReadyConditionType returns a SveltosHelmReleaseReady +// type per service to be used in status conditions. +func HelmReleaseReadyConditionType(releaseNamespace, releaseName string) string { + return fmt.Sprintf( + "%s.%s/%s", + releaseNamespace, + releaseName, + hmc.SveltosHelmReleaseReadyCondition, + ) +} diff --git a/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml b/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml index 59dacde72..c1ef43e63 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml @@ -207,6 +207,81 @@ spec: description: ObservedGeneration is the last observed generation. format: int64 type: integer + services: + description: Services contains details for the state of services. + items: + description: ServiceStatus contains details for the state of services. + properties: + clusterName: + description: ClusterName is the name of the associated cluster. + type: string + clusterNamespace: + description: ClusterNamespace is the namespace of the associated + cluster. + type: string + conditions: + description: Conditions contains details for the current state + of managed services. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + required: + - clusterName + type: object + type: array type: object type: object served: true diff --git a/templates/provider/hmc/templates/crds/hmc.mirantis.com_multiclusterservices.yaml b/templates/provider/hmc/templates/crds/hmc.mirantis.com_multiclusterservices.yaml index dc2a7fa93..1cbbc3440 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_multiclusterservices.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_multiclusterservices.yaml @@ -141,11 +141,145 @@ spec: type: boolean type: object status: - description: |- - MultiClusterServiceStatus defines the observed state of MultiClusterService - - If this status ends up being common with ManagedClusterStatus, - then make a common status struct that can be shared by both. + description: MultiClusterServiceStatus defines the observed state of MultiClusterService. + properties: + conditions: + description: Conditions contains details for the current state of + the ManagedCluster + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: ObservedGeneration is the last observed generation. + format: int64 + type: integer + services: + description: Services contains details for the state of services. + items: + description: ServiceStatus contains details for the state of services. + properties: + clusterName: + description: ClusterName is the name of the associated cluster. + type: string + clusterNamespace: + description: ClusterNamespace is the namespace of the associated + cluster. + type: string + conditions: + description: Conditions contains details for the current state + of managed services. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + required: + - clusterName + type: object + type: array type: object type: object served: true diff --git a/templates/provider/hmc/templates/rbac/controller/roles.yaml b/templates/provider/hmc/templates/rbac/controller/roles.yaml index 49c3f27a1..1a5d54174 100644 --- a/templates/provider/hmc/templates/rbac/controller/roles.yaml +++ b/templates/provider/hmc/templates/rbac/controller/roles.yaml @@ -174,6 +174,7 @@ rules: resources: - profiles - clusterprofiles + - clustersummaries verbs: {{ include "rbac.editorVerbs" . | nindent 4 }} - apiGroups: - hmc.mirantis.com