From 3e1019fae814a17ac3c5b0e2c1b05e58fbe80bb4 Mon Sep 17 00:00:00 2001 From: Francesco Giudici Date: Wed, 4 Dec 2024 09:27:58 +0100 Subject: [PATCH 1/8] operator: add ManagedOSChangelog CRD Signed-off-by: Francesco Giudici --- api/v1beta1/managedoschangelog_types.go | 86 +++++++++++ api/v1beta1/zz_generated.deepcopy.go | 96 ++++++++++++ ...emental.cattle.io_managedoschangelogs.yaml | 145 ++++++++++++++++++ 3 files changed, 327 insertions(+) create mode 100644 api/v1beta1/managedoschangelog_types.go create mode 100644 config/crd/bases/elemental.cattle.io_managedoschangelogs.yaml diff --git a/api/v1beta1/managedoschangelog_types.go b/api/v1beta1/managedoschangelog_types.go new file mode 100644 index 000000000..35a904e62 --- /dev/null +++ b/api/v1beta1/managedoschangelog_types.go @@ -0,0 +1,86 @@ +/* +Copyright © 2022 - 2024 SUSE LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + ManagedOSChangelogFinalizer = "managedoschangelog.elemental.cattle.io" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +type ManagedOSChangelog struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ManagedOSChangelogSpec `json:"spec,omitempty"` + Status ManagedOSChangelogStatus `json:"status,omitempty"` +} + +type ManagedOSChangelogSpec struct { + // ManagedOSVersion the name of the ManagedOSVersion resource for which the changelog + // should be generated. + ManagedOSVersion string `json:"osVersion,omitempty"` + // LifetimeMinutes the time at which the changelog data will be cleaned up. + // Default is 60 minutes, set to 0 to disable. + // +kubebuilder:default:=60 + // +optional + LifetimeMinutes int32 `json:"cleanupAfterMinutes"` + // Refresh triggers to build again a cleaned up changelog. + // +optional + Refresh bool `json:"refresh"` +} + +type ChangelogState string + +const ( + ChangelogInit ChangelogState = "Initialized" + ChangelogStarted ChangelogState = "Started" + ChangelogCompleted ChangelogState = "Completed" + ChangelogFailed ChangelogState = "Failed" + ChangelogNotStarted ChangelogState = "NotStarted" +) + +type ManagedOSChangelogStatus struct { + // Conditions describe the state of the changelog object. + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + // ChangelogURL the URL where changelog data can be displayed + // +optional + ChangelogURL string `json:"changelogURL,omitempty"` + // State reflect the state of the changelog generation process. + // +kubebuilder:validation:Enum=Initialized;Started;Completed;Failed;NotStarted + // +optional + State ChangelogState `json:"state,omitempty"` +} + +// +kubebuilder:object:root=true + +// ManagedOSChangelogList contains a list of ManagedOSChangelogs. +type ManagedOSChangelogList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ManagedOSChangelog `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ManagedOSChangelog{}, &ManagedOSChangelogList{}) +} diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 9f3a2e3fa..67c5c78a4 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -644,6 +644,102 @@ func (in *MachineRegistrationStatus) DeepCopy() *MachineRegistrationStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedOSChangelog) DeepCopyInto(out *ManagedOSChangelog) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedOSChangelog. +func (in *ManagedOSChangelog) DeepCopy() *ManagedOSChangelog { + if in == nil { + return nil + } + out := new(ManagedOSChangelog) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ManagedOSChangelog) 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 *ManagedOSChangelogList) DeepCopyInto(out *ManagedOSChangelogList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ManagedOSChangelog, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedOSChangelogList. +func (in *ManagedOSChangelogList) DeepCopy() *ManagedOSChangelogList { + if in == nil { + return nil + } + out := new(ManagedOSChangelogList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ManagedOSChangelogList) 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 *ManagedOSChangelogSpec) DeepCopyInto(out *ManagedOSChangelogSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedOSChangelogSpec. +func (in *ManagedOSChangelogSpec) DeepCopy() *ManagedOSChangelogSpec { + if in == nil { + return nil + } + out := new(ManagedOSChangelogSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedOSChangelogStatus) DeepCopyInto(out *ManagedOSChangelogStatus) { + *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 ManagedOSChangelogStatus. +func (in *ManagedOSChangelogStatus) DeepCopy() *ManagedOSChangelogStatus { + if in == nil { + return nil + } + out := new(ManagedOSChangelogStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagedOSImage) DeepCopyInto(out *ManagedOSImage) { *out = *in diff --git a/config/crd/bases/elemental.cattle.io_managedoschangelogs.yaml b/config/crd/bases/elemental.cattle.io_managedoschangelogs.yaml new file mode 100644 index 000000000..8bb1af207 --- /dev/null +++ b/config/crd/bases/elemental.cattle.io_managedoschangelogs.yaml @@ -0,0 +1,145 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: managedoschangelogs.elemental.cattle.io +spec: + group: elemental.cattle.io + names: + kind: ManagedOSChangelog + listKind: ManagedOSChangelogList + plural: managedoschangelogs + singular: managedoschangelog + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + 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: + cleanupAfterMinutes: + default: 60 + description: |- + LifetimeMinutes the time at which the changelog data will be cleaned up. + Default is 60 minutes, set to 0 to disable. + format: int32 + type: integer + osVersion: + description: |- + ManagedOSVersion the name of the ManagedOSVersion resource for which the changelog + should be generated. + type: string + refresh: + description: Refresh triggers to build again a cleaned up changelog. + type: boolean + type: object + status: + properties: + changelogURL: + description: ChangelogURL the URL where changelog data can be displayed + type: string + conditions: + description: Conditions describe the state of the changelog object. + 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 + state: + description: State reflect the state of the changelog generation process. + enum: + - Initialized + - Started + - Completed + - Failed + - NotStarted + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} From 1e52b0add17bd9447880e1e72e5ecbaa64ebcbab Mon Sep 17 00:00:00 2001 From: Francesco Giudici Date: Wed, 4 Dec 2024 09:37:45 +0100 Subject: [PATCH 2/8] operator: add ManagedOSChangelog controller Signed-off-by: Francesco Giudici --- cmd/operator/operator/root.go | 6 ++ config/rbac/bases/role.yaml | 26 +++++ controllers/managedoschangelog_controller.go | 100 ++++++++++++++++++ .../managedoschangelog_controller_test.go | 84 +++++++++++++++ 4 files changed, 216 insertions(+) create mode 100644 controllers/managedoschangelog_controller.go create mode 100644 controllers/managedoschangelog_controller_test.go diff --git a/cmd/operator/operator/root.go b/cmd/operator/operator/root.go index dca88dd09..631e15cd7 100644 --- a/cmd/operator/operator/root.go +++ b/cmd/operator/operator/root.go @@ -324,4 +324,10 @@ func setupReconcilers(mgr ctrl.Manager, config *rootConfig) { setupLog.Error(err, "unable to create reconciler", "controller", "ManagedOSVersion") os.Exit(1) } + if err := (&controllers.ManagedOSChangelogReconciler{ + Client: mgr.GetClient(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "Unable to create reconciler", "controller", "ManagedOSChangelog") + os.Exit(1) + } } diff --git a/config/rbac/bases/role.yaml b/config/rbac/bases/role.yaml index 2ce2131b6..0db6b96c0 100644 --- a/config/rbac/bases/role.yaml +++ b/config/rbac/bases/role.yaml @@ -128,6 +128,32 @@ rules: - get - patch - update +- apiGroups: + - elemental.cattle.io + resources: + - managedoschangelogs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - elemental.cattle.io + resources: + - managedoschangelogs/finalizers + verbs: + - update +- apiGroups: + - elemental.cattle.io + resources: + - managedoschangelogs/status + verbs: + - get + - patch + - update - apiGroups: - elemental.cattle.io resources: diff --git a/controllers/managedoschangelog_controller.go b/controllers/managedoschangelog_controller.go new file mode 100644 index 000000000..6e65b78c9 --- /dev/null +++ b/controllers/managedoschangelog_controller.go @@ -0,0 +1,100 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + errorutils "k8s.io/apimachinery/pkg/util/errors" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + elementalv1 "github.com/rancher/elemental-operator/api/v1beta1" +) + +// ManagedOSChangelogReconciler reconciles a ManagedOSChangelog object +type ManagedOSChangelogReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=elemental.cattle.io,resources=managedoschangelogs,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=elemental.cattle.io,resources=managedoschangelogs/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=elemental.cattle.io,resources=managedoschangelogs/finalizers,verbs=update + +// SetupWithManager sets up the controller with the Manager. +func (r *ManagedOSChangelogReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&elementalv1.ManagedOSChangelog{}). + Complete(r) +} + +func (r *ManagedOSChangelogReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := ctrl.LoggerFrom(ctx) + + changelog := &elementalv1.ManagedOSChangelog{} + if err := r.Get(ctx, req.NamespacedName, changelog); err != nil { + if apierrors.IsNotFound(err) { + logger.V(5).Info("Object was not found, not an error") + return ctrl.Result{}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to get managedoschangelog object: %w", err) + } + + patchBase := client.MergeFrom(changelog.DeepCopy()) + + // Collect errors aas an aggregate to return together after all patches have been performed. + var errs []error + + result, err := r.reconcile(ctx, changelog) + if err != nil { + errs = append(errs, fmt.Errorf("error reconciling changelog object: %w", err)) + } + + changelogStatusCopy := changelog.Status.DeepCopy() // Patch call will erase the status + + if err := r.Patch(ctx, changelog, patchBase); err != nil && !apierrors.IsNotFound(err) { + errs = append(errs, fmt.Errorf("failed to patch status for managedoschangelog object: %w", err)) + } + + changelog.Status = *changelogStatusCopy + + if err := r.Status().Patch(ctx, changelog, patchBase); err != nil && !apierrors.IsNotFound(err) { + errs = append(errs, fmt.Errorf("failed to patch status for changelog object: %w", err)) + } + + if len(errs) > 0 { + return ctrl.Result{}, errorutils.NewAggregate(errs) + } + + return result, nil +} + +func (r *ManagedOSChangelogReconciler) reconcile(ctx context.Context, changelog *elementalv1.ManagedOSChangelog) (ctrl.Result, error) { + logger := ctrl.LoggerFrom(ctx) + + logger.Info("Reconciling managedoschangelog object") + + if changelog.GetDeletionTimestamp() != nil { + return ctrl.Result{}, nil + } + + return ctrl.Result{}, nil +} diff --git a/controllers/managedoschangelog_controller_test.go b/controllers/managedoschangelog_controller_test.go new file mode 100644 index 000000000..b80a9eaf8 --- /dev/null +++ b/controllers/managedoschangelog_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + elementalv1beta1 "github.com/rancher/elemental-operator/api/v1beta1" +) + +var _ = Describe("ManagedOSChangelog Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + managedoschangelog := &elementalv1beta1.ManagedOSChangelog{} + + BeforeEach(func() { + By("creating the custom resource for the Kind ManagedOSChangelog") + err := cl.Get(ctx, typeNamespacedName, managedoschangelog) + if err != nil && errors.IsNotFound(err) { + resource := &elementalv1beta1.ManagedOSChangelog{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(cl.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &elementalv1beta1.ManagedOSChangelog{} + err := cl.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance ManagedOSChangelog") + Expect(cl.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &ManagedOSChangelogReconciler{ + Client: cl, + Scheme: cl.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) From fef0f094a7ff8ceede182ac541c800218106d376 Mon Sep 17 00:00:00 2001 From: Francesco Giudici Date: Wed, 4 Dec 2024 09:44:37 +0100 Subject: [PATCH 3/8] make build-manifest Signed-off-by: Francesco Giudici --- .../templates/rbac.yaml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.obs/chartfile/elemental-operator-helm/templates/rbac.yaml b/.obs/chartfile/elemental-operator-helm/templates/rbac.yaml index 7d08b802e..dcbf25689 100644 --- a/.obs/chartfile/elemental-operator-helm/templates/rbac.yaml +++ b/.obs/chartfile/elemental-operator-helm/templates/rbac.yaml @@ -182,6 +182,32 @@ rules: - get - patch - update +- apiGroups: + - elemental.cattle.io + resources: + - managedoschangelogs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - elemental.cattle.io + resources: + - managedoschangelogs/finalizers + verbs: + - update +- apiGroups: + - elemental.cattle.io + resources: + - managedoschangelogs/status + verbs: + - get + - patch + - update - apiGroups: - elemental.cattle.io resources: From c1baed65e6ad997fb18e8a02bd364cb10d7fc316 Mon Sep 17 00:00:00 2001 From: Francesco Giudici Date: Wed, 4 Dec 2024 09:48:52 +0100 Subject: [PATCH 4/8] ManagedOSChangelog: add CRD to kustomize file config/crd/kustomization.yaml <-- add managedoschangelogs yaml make build-crds Signed-off-by: Francesco Giudici --- .../templates/crds.yaml | 152 ++++++++++++++++++ config/crd/kustomization.yaml | 3 +- 2 files changed, 154 insertions(+), 1 deletion(-) diff --git a/.obs/chartfile/elemental-operator-crds-helm/templates/crds.yaml b/.obs/chartfile/elemental-operator-crds-helm/templates/crds.yaml index a2922ae34..7c549d4a3 100644 --- a/.obs/chartfile/elemental-operator-crds-helm/templates/crds.yaml +++ b/.obs/chartfile/elemental-operator-crds-helm/templates/crds.yaml @@ -1140,6 +1140,158 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + app.kubernetes.io/instance: '{{ .Release.Name }}' + app.kubernetes.io/part-of: Elemental Operator + app.kubernetes.io/version: '{{ .Chart.Version }}' + controller-gen.kubebuilder.io/version: v0.14.0 + labels: + cluster.x-k8s.io/provider: infrastructure-elemental + cluster.x-k8s.io/v1beta1: v1beta1 + release-name: '{{ .Release.Name }}' + name: managedoschangelogs.elemental.cattle.io +spec: + group: elemental.cattle.io + names: + kind: ManagedOSChangelog + listKind: ManagedOSChangelogList + plural: managedoschangelogs + singular: managedoschangelog + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + 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: + cleanupAfterMinutes: + default: 60 + description: |- + LifetimeMinutes the time at which the changelog data will be cleaned up. + Default is 60 minutes, set to 0 to disable. + format: int32 + type: integer + osVersion: + description: |- + ManagedOSVersion the name of the ManagedOSVersion resource for which the changelog + should be generated. + type: string + refresh: + description: Refresh triggers to build again a cleaned up changelog. + type: boolean + type: object + status: + properties: + changelogURL: + description: ChangelogURL the URL where changelog data can be displayed + type: string + conditions: + description: Conditions describe the state of the changelog object. + 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 + state: + description: State reflect the state of the changelog generation process. + enum: + - Initialized + - Started + - Completed + - Failed + - NotStarted + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: app.kubernetes.io/instance: '{{ .Release.Name }}' diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index ad7132ddf..fc7fabac2 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,11 +6,12 @@ resources: - bases/elemental.cattle.io_machineinventoryselectors.yaml - bases/elemental.cattle.io_machineinventoryselectortemplates.yaml - bases/elemental.cattle.io_machineregistrations.yaml +- bases/elemental.cattle.io_metadata.yaml - bases/elemental.cattle.io_managedosversionchannels.yaml +- bases/elemental.cattle.io_managedoschangelogs.yaml - bases/elemental.cattle.io_managedosimages.yaml - bases/elemental.cattle.io_managedosversions.yaml - bases/elemental.cattle.io_seedimages.yaml -- bases/elemental.cattle.io_metadata.yaml # +kubebuilder:scaffold:crdkustomizeresource commonLabels: From c0b588c1902ab9f1ce7ccdf7f1791fcc76971897 Mon Sep 17 00:00:00 2001 From: Francesco Giudici Date: Thu, 5 Dec 2024 15:02:58 +0100 Subject: [PATCH 5/8] managedoschangelog: implement controller logic Signed-off-by: Francesco Giudici --- api/v1beta1/condition_consts.go | 19 + api/v1beta1/managedoschangelog_types.go | 3 + cmd/operator/operator/root.go | 5 +- controllers/managedoschangelog_controller.go | 391 ++++++++++++++++++- 4 files changed, 416 insertions(+), 2 deletions(-) diff --git a/api/v1beta1/condition_consts.go b/api/v1beta1/condition_consts.go index ff6b7aba9..81acf0135 100644 --- a/api/v1beta1/condition_consts.go +++ b/api/v1beta1/condition_consts.go @@ -163,6 +163,25 @@ const ( ResourcesSuccessfullyCreatedReason = "ResourcesSuccessfullyCreated" ) +// Managed OS Changelog conditions +const ( + // WorkerPodReadyCondition is the condition type tracking the state of the worked pod. + WorkerPodReadyCondition = "WorkerPodReady" + // WorkerPodNotStartedReason documents worker pod not being started. + WorkerPodNotStartedReason = "WorkerPodNotStarted" + // WorkerPodInitializing documents worker pod being initialized. + WorkerPodInitReason = "WorkerPodInitializing" + // WorkerPodCompletionFailureReason documents failure to successfully complete the worker pod tasks. + WorkerPodCompletionFailureReason = "WorkerPodCompletionFailure" + // WorkerPodCompletionSuccessReason documents worker pod completed its tasks and is successfully running. + WorkerPodCompletionSuccessReason = "WorkerPodCompletionSuccess" + // WorkerPodDeadline documents worker pod deadline has elapsed. + WorkerPodDeadline = "WorkerPodDeadline" + // WorkerPodUnknown documents worker pod in an unknown status. + WorkerPodUnknown = "WorkerPodUnknown" +) + +// Seed Image conditions const ( // SeedImageConditionReady is the condition type tracking the state of the seed image build pod. SeedImageConditionReady = "SeedImageReady" diff --git a/api/v1beta1/managedoschangelog_types.go b/api/v1beta1/managedoschangelog_types.go index 35a904e62..a2bb57f34 100644 --- a/api/v1beta1/managedoschangelog_types.go +++ b/api/v1beta1/managedoschangelog_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1beta1 import ( + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -36,6 +37,8 @@ type ManagedOSChangelog struct { } type ManagedOSChangelogSpec struct { + // ManagedOSVersionChannelRef a referemce to the related ManagedOSVersionChannel. + ManagedOSVersionChannelRef *corev1.ObjectReference `json:"channelRef"` // ManagedOSVersion the name of the ManagedOSVersion resource for which the changelog // should be generated. ManagedOSVersion string `json:"osVersion,omitempty"` diff --git a/cmd/operator/operator/root.go b/cmd/operator/operator/root.go index 631e15cd7..6bf8833e0 100644 --- a/cmd/operator/operator/root.go +++ b/cmd/operator/operator/root.go @@ -325,7 +325,10 @@ func setupReconcilers(mgr ctrl.Manager, config *rootConfig) { os.Exit(1) } if err := (&controllers.ManagedOSChangelogReconciler{ - Client: mgr.GetClient(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + WorkerPodImage: config.seedimageImage, + WorkerPodImagePullPolicy: corev1.PullPolicy(config.seedimageImagePullPolicy), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "Unable to create reconciler", "controller", "ManagedOSChangelog") os.Exit(1) diff --git a/controllers/managedoschangelog_controller.go b/controllers/managedoschangelog_controller.go index 6e65b78c9..fcd1c599c 100644 --- a/controllers/managedoschangelog_controller.go +++ b/controllers/managedoschangelog_controller.go @@ -19,30 +19,47 @@ package controllers import ( "context" "fmt" + "regexp" + "strings" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" errorutils "k8s.io/apimachinery/pkg/util/errors" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" elementalv1 "github.com/rancher/elemental-operator/api/v1beta1" + managementv3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3" ) // ManagedOSChangelogReconciler reconciles a ManagedOSChangelog object type ManagedOSChangelogReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + WorkerPodImage string + WorkerPodImagePullPolicy corev1.PullPolicy } // +kubebuilder:rbac:groups=elemental.cattle.io,resources=managedoschangelogs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=elemental.cattle.io,resources=managedoschangelogs/status,verbs=get;update;patch // +kubebuilder:rbac:groups=elemental.cattle.io,resources=managedoschangelogs/finalizers,verbs=update +// +// +kubebuilder:rbac:groups="",namespace=fleet-default,resources=pods,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",namespace=fleet-default,resources=pods/status,verbs=get +// +kubebuilder:rbac:groups="",namespace=fleet-default,resources=services,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",namespace=fleet-default,resources=services/status,verbs=get // SetupWithManager sets up the controller with the Manager. func (r *ManagedOSChangelogReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&elementalv1.ManagedOSChangelog{}). + Owns(&corev1.Pod{}). Complete(r) } @@ -96,5 +113,377 @@ func (r *ManagedOSChangelogReconciler) reconcile(ctx context.Context, changelog return ctrl.Result{}, nil } + // Init the Ready condition as we want it to be the first one displayed + if readyCond := meta.FindStatusCondition(changelog.Status.Conditions, elementalv1.ReadyCondition); readyCond == nil { + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.ReadyCondition, + Reason: elementalv1.ResourcesNotCreatedYet, + Status: metav1.ConditionUnknown, + }) + } + + channel := &elementalv1.ManagedOSVersionChannel{} + + if err := r.reconcileManagedOSChangelogOwner(ctx, changelog, channel); err != nil { + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.ReadyCondition, + Status: metav1.ConditionFalse, + Reason: elementalv1.SetOwnerFailureReason, + Message: err.Error(), + }) + return ctrl.Result{}, fmt.Errorf("failed to set managedoschangelog owner: %w", err) + } + + if err := r.reconcileWorkerPod(ctx, changelog, channel); err != nil { + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.ReadyCondition, + Status: metav1.ConditionFalse, + Reason: elementalv1.PodCreationFailureReason, + Message: err.Error(), + }) + return ctrl.Result{}, fmt.Errorf("failed to reconcile pod: %w", err) + } + + if err := r.reconcileWorkerService(ctx, changelog); err != nil { + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.ReadyCondition, + Status: metav1.ConditionFalse, + Reason: elementalv1.ServiceCreationFailureReason, + Message: err.Error(), + }) + return ctrl.Result{}, fmt.Errorf("failed to create service: %w", err) + } + + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.ReadyCondition, + Reason: elementalv1.ResourcesSuccessfullyCreatedReason, + Status: metav1.ConditionTrue, + Message: "resources created successfully", + }) + return ctrl.Result{}, nil } + +func (r *ManagedOSChangelogReconciler) reconcileManagedOSChangelogOwner(ctx context.Context, + changelog *elementalv1.ManagedOSChangelog, channel *elementalv1.ManagedOSVersionChannel) error { + if err := r.Get(ctx, types.NamespacedName{ + Name: changelog.Spec.ManagedOSVersionChannelRef.Name, + Namespace: changelog.Spec.ManagedOSVersionChannelRef.Namespace, + }, channel); err != nil { + return err + } + + for _, o := range channel.OwnerReferences { + if o.UID == channel.UID { + return nil + } + } + + return controllerutil.SetOwnerReference(channel, changelog, r.Scheme) +} + +func (r *ManagedOSChangelogReconciler) reconcileWorkerPod(ctx context.Context, + changelog *elementalv1.ManagedOSChangelog, channel *elementalv1.ManagedOSVersionChannel) error { + logger := ctrl.LoggerFrom(ctx) + + logger.Info("Reconciling Pod") + + podChannelImg := "" + if val, ok := channel.Spec.Options["image"]; !ok { + return fmt.Errorf("managedosversionchannel 'image' option is missing") + } else { + // drop quotes from the image val + podChannelImg = regexp.MustCompile(`^"(.*)"$`).ReplaceAllString(string(val.Raw), `$1`) + } + + pod := &corev1.Pod{} + err := r.Get(ctx, types.NamespacedName{Name: changelog.Name, Namespace: changelog.Namespace}, pod) + if err != nil && !apierrors.IsNotFound(err) { + return err + } + if err == nil { + logger.V(5).Info("Pod already created") + return r.updateStatusFromWorkerPod(ctx, changelog, pod) + } + + logger.V(5).Info("Creating pod") + + pod = r.fillWorkerPod(changelog, podChannelImg) + if err := controllerutil.SetControllerReference(changelog, pod, r.Scheme); err != nil { + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.WorkerPodReadyCondition, + Status: metav1.ConditionFalse, + Reason: elementalv1.WorkerPodNotStartedReason, + Message: err.Error(), + }) + return err + } + + if err := r.Create(ctx, pod); err != nil && !apierrors.IsAlreadyExists(err) { + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.WorkerPodReadyCondition, + Status: metav1.ConditionFalse, + Reason: elementalv1.WorkerPodNotStartedReason, + Message: err.Error(), + }) + return err + } + + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.WorkerPodReadyCondition, + Status: metav1.ConditionFalse, + Reason: elementalv1.WorkerPodInitReason, + Message: "worker pod started", + }) + return nil +} + +func (r *ManagedOSChangelogReconciler) reconcileWorkerService(ctx context.Context, + changelog *elementalv1.ManagedOSChangelog) error { + logger := ctrl.LoggerFrom(ctx) + + logger.Info("Reconciling Service") + + deadlineElapsed := false + if readyCondition := meta.FindStatusCondition(changelog.Status.Conditions, elementalv1.WorkerPodReadyCondition); readyCondition != nil && + readyCondition.Status == metav1.ConditionTrue && + readyCondition.Reason == elementalv1.WorkerPodDeadline { + deadlineElapsed = true + } + + foundSvc := &corev1.Service{} + err := r.Get(ctx, types.NamespacedName{Name: changelog.Name, Namespace: changelog.Namespace}, foundSvc) + if err != nil && !apierrors.IsNotFound(err) { + return err + } + + if err == nil { + logger.V(5).Info("Service already there") + + // ensure the service was created by us + for _, owner := range foundSvc.GetOwnerReferences() { + if owner.UID == changelog.UID { + if deadlineElapsed { + logger.V(5).Info("Worker pod deadline passed, delete associated service", "service", foundSvc.Name) + if err := r.Delete(ctx, foundSvc); err != nil { + return fmt.Errorf("failed to delete service %s: %w", foundSvc.Name, err) + } + } + return nil + } + } + + return fmt.Errorf("service already exists and was not created by this controller") + } + + if deadlineElapsed { + return nil + } + + logger.V(5).Info("Creating service") + + // TODO: move fillBuildImageService() to a shared package (actually is in the SeedImage controller code) + service := fillBuildImageService(changelog.Name, changelog.Namespace) + + if err := controllerutil.SetControllerReference(changelog, service, r.Scheme); err != nil { + return err + } + + if err := r.Create(ctx, service); err != nil && !apierrors.IsAlreadyExists(err) { + return err + } + + return nil +} + +func (r *ManagedOSChangelogReconciler) updateStatusFromWorkerPod(ctx context.Context, + changelog *elementalv1.ManagedOSChangelog, pod *corev1.Pod) error { + logger := ctrl.LoggerFrom(ctx) + + if !pod.DeletionTimestamp.IsZero() { + logger.V(5).Info("Wait the worker Pod to terminate") + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.WorkerPodReadyCondition, + Status: metav1.ConditionFalse, + Reason: elementalv1.WorkerPodNotStartedReason, + Message: "wait old worker Pod termination", + }) + return nil + } + + if meta.IsStatusConditionTrue(changelog.Status.Conditions, elementalv1.WorkerPodReadyCondition) && + pod.Status.Phase != corev1.PodSucceeded { + return nil + } + + logger.V(5).Info("Sync WorkerPodReady condition from worker Pod", "pod-phase", pod.Status.Phase) + + switch pod.Status.Phase { + case corev1.PodPending: + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.WorkerPodReadyCondition, + Status: metav1.ConditionFalse, + Reason: elementalv1.WorkerPodInitReason, + Message: "changelog retrieval ongoing", + }) + return nil + case corev1.PodRunning: + rancherURL, err := r.getRancherServerAddress(ctx) + if err != nil { + errMsg := fmt.Errorf("failed to get Rancher Server Address: %w", err) + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.WorkerPodReadyCondition, + Status: metav1.ConditionFalse, + Reason: elementalv1.WorkerPodCompletionFailureReason, + Message: errMsg.Error(), + }) + return errMsg + } + // Let's check here we have an associated Service, so the changelog could be displayed + if err := r.Get(ctx, types.NamespacedName{Name: changelog.Name, Namespace: changelog.Namespace}, + &corev1.Service{}); err != nil { + errMsg := fmt.Errorf("failed to get associated service: %w", err) + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.WorkerPodReadyCondition, + Status: metav1.ConditionFalse, + Reason: elementalv1.WorkerPodCompletionFailureReason, + Message: errMsg.Error(), + }) + return errMsg + } + token := changelog.Spec.ManagedOSVersion + changelog.Status.ChangelogURL = fmt.Sprintf("https://%s/elemental/changelog/%s/", rancherURL, token) + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.WorkerPodReadyCondition, + Status: metav1.ConditionTrue, + Reason: elementalv1.WorkerPodCompletionSuccessReason, + Message: "changelog available", + }) + return nil + case corev1.PodFailed: + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.WorkerPodReadyCondition, + Status: metav1.ConditionFalse, + Reason: elementalv1.WorkerPodCompletionFailureReason, + Message: "worker pod failed", + }) + return nil + case corev1.PodSucceeded: + if err := r.Delete(ctx, pod); err != nil { + errMsg := fmt.Errorf("failed to delete worker pod: %w", err) + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.WorkerPodReadyCondition, + Status: metav1.ConditionFalse, + Reason: elementalv1.WorkerPodDeadline, + Message: errMsg.Error(), + }) + return errMsg + } + changelog.Status.ChangelogURL = "" + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.WorkerPodReadyCondition, + Status: metav1.ConditionTrue, + Reason: elementalv1.WorkerPodDeadline, + Message: "worker pod deadline elapsed", + }) + return nil + default: + meta.SetStatusCondition(&changelog.Status.Conditions, metav1.Condition{ + Type: elementalv1.WorkerPodReadyCondition, + Status: metav1.ConditionUnknown, + Reason: elementalv1.WorkerPodUnknown, + Message: fmt.Sprintf("pod phase %s", pod.Status.Phase), + }) + return nil + + } +} + +func (r *ManagedOSChangelogReconciler) fillWorkerPod(changelog *elementalv1.ManagedOSChangelog, + channelImage string) *corev1.Pod { + name := changelog.Name + namespace := changelog.Namespace + image := r.WorkerPodImage + imagePullPolicy := r.WorkerPodImagePullPolicy + deadline := changelog.Spec.LifetimeMinutes + diskMaxSize, _ := resource.ParseQuantity("100M") + extractCmd := []string{ + "cp", + fmt.Sprintf("/changelogs/%s.updates.log", changelog.Spec.ManagedOSVersion), + "/data/", + } + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: map[string]string{"app.kubernetes.io/name": name}, + }, + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + { + Name: "extract", + Image: channelImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Command: []string{"/bin/busybox"}, + Args: extractCmd, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "changelog-storage", + MountPath: "/data", + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: "serve", + Image: image, + ImagePullPolicy: imagePullPolicy, + Ports: []corev1.ContainerPort{ + { + Name: "http", + ContainerPort: 80, + }, + }, + Args: []string{"-d", "/srv", "-t", fmt.Sprintf("%d", deadline*60)}, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "changelog-storage", + MountPath: "/srv", + }, + }, + }, + }, + RestartPolicy: corev1.RestartPolicyNever, + Volumes: []corev1.Volume{ + { + Name: "changelog-storage", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + SizeLimit: &diskMaxSize, + }, + }, + }, + }, + }, + } + return pod +} + +func (r *ManagedOSChangelogReconciler) getRancherServerAddress(ctx context.Context) (string, error) { + logger := ctrl.LoggerFrom(ctx) + + setting := &managementv3.Setting{} + if err := r.Get(ctx, types.NamespacedName{Name: "server-url"}, setting); err != nil { + return "", fmt.Errorf("failed to get server url setting: %w", err) + } + + if setting.Value == "" { + err := fmt.Errorf("server-url is not set") + logger.Error(err, "can't get server-url") + return "", err + } + + return strings.TrimPrefix(setting.Value, "https://"), nil +} From db739310b1e1a4fc9bc279d2be40cdec3a59aae0 Mon Sep 17 00:00:00 2001 From: Francesco Giudici Date: Fri, 6 Dec 2024 16:35:30 +0100 Subject: [PATCH 6/8] make generate Signed-off-by: Francesco Giudici --- api/v1beta1/zz_generated.deepcopy.go | 7 ++- ...emental.cattle.io_managedoschangelogs.yaml | 47 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 67c5c78a4..3bf882adf 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -649,7 +649,7 @@ func (in *ManagedOSChangelog) DeepCopyInto(out *ManagedOSChangelog) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -706,6 +706,11 @@ func (in *ManagedOSChangelogList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagedOSChangelogSpec) DeepCopyInto(out *ManagedOSChangelogSpec) { *out = *in + if in.ManagedOSVersionChannelRef != nil { + in, out := &in.ManagedOSVersionChannelRef, &out.ManagedOSVersionChannelRef + *out = new(v1.ObjectReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedOSChangelogSpec. diff --git a/config/crd/bases/elemental.cattle.io_managedoschangelogs.yaml b/config/crd/bases/elemental.cattle.io_managedoschangelogs.yaml index 8bb1af207..6d8f8c80e 100644 --- a/config/crd/bases/elemental.cattle.io_managedoschangelogs.yaml +++ b/config/crd/bases/elemental.cattle.io_managedoschangelogs.yaml @@ -37,6 +37,51 @@ spec: type: object spec: properties: + channelRef: + description: ManagedOSVersionChannelRef a referemce to the related + ManagedOSVersionChannel. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + TODO: this design is not final and this field is subject to change in the future. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic cleanupAfterMinutes: default: 60 description: |- @@ -52,6 +97,8 @@ spec: refresh: description: Refresh triggers to build again a cleaned up changelog. type: boolean + required: + - channelRef type: object status: properties: From 07c8bd45f0e38a68347baf54623393c7be895764 Mon Sep 17 00:00:00 2001 From: Francesco Giudici Date: Fri, 6 Dec 2024 16:36:15 +0100 Subject: [PATCH 7/8] make build-manifests Signed-off-by: Francesco Giudici --- .../templates/crds.yaml | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/.obs/chartfile/elemental-operator-crds-helm/templates/crds.yaml b/.obs/chartfile/elemental-operator-crds-helm/templates/crds.yaml index 7c549d4a3..749efcbc7 100644 --- a/.obs/chartfile/elemental-operator-crds-helm/templates/crds.yaml +++ b/.obs/chartfile/elemental-operator-crds-helm/templates/crds.yaml @@ -1183,6 +1183,51 @@ spec: type: object spec: properties: + channelRef: + description: ManagedOSVersionChannelRef a referemce to the related + ManagedOSVersionChannel. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + TODO: this design is not final and this field is subject to change in the future. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic cleanupAfterMinutes: default: 60 description: |- @@ -1198,6 +1243,8 @@ spec: refresh: description: Refresh triggers to build again a cleaned up changelog. type: boolean + required: + - channelRef type: object status: properties: From f970d6712c67545d6a40cceb2ae86b1ed04e9e26 Mon Sep 17 00:00:00 2001 From: Francesco Giudici Date: Fri, 6 Dec 2024 16:55:04 +0100 Subject: [PATCH 8/8] changelog: add proxy in the operator http server this will allow to display the changelog using the Rancher UI URL: https:///elemental/changelog/ which is set in the ManagedOSChangelog.status.changelogURL Signed-off-by: Francesco Giudici --- pkg/server/api_changelog.go | 109 ++++++++++++++++++++++++++++++++++++ pkg/server/server.go | 6 +- 2 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 pkg/server/api_changelog.go diff --git a/pkg/server/api_changelog.go b/pkg/server/api_changelog.go new file mode 100644 index 000000000..994820f22 --- /dev/null +++ b/pkg/server/api_changelog.go @@ -0,0 +1,109 @@ +/* +Copyright © 2022 - 2024 SUSE LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "fmt" + "net/http" + "net/http/httputil" + "net/url" + "path" + "strings" + + corev1 "k8s.io/api/core/v1" + + elementalv1 "github.com/rancher/elemental-operator/api/v1beta1" + "k8s.io/apimachinery/pkg/types" +) + +func (i *InventoryServer) apiChangelog(resp http.ResponseWriter, req *http.Request, splittedPath []string) error { + var err error + var changelog *elementalv1.ManagedOSChangelog + + // expected splittedPath = {"changelog", {ManagedOSVersion.Name}} + if len(splittedPath) < 2 { + err = fmt.Errorf("unexpected path: %v", splittedPath) + http.Error(resp, err.Error(), http.StatusNotFound) + return err + } + osVersion := splittedPath[1] + filename := "" + if len(splittedPath) == 3 { + filename = splittedPath[2] + } + + if changelog, err = i.getManagedOSChangelog(osVersion); err != nil { + http.Error(resp, err.Error(), http.StatusNotFound) + return err + } + + svc := &corev1.Service{} + if err = i.Get(i, types.NamespacedName{Namespace: changelog.Namespace, Name: changelog.Name}, svc); err != nil { + errMsg := fmt.Errorf("failed to get service for changelog %s/%s: %w", changelog.Namespace, changelog.Name, err) + http.Error(resp, errMsg.Error(), http.StatusInternalServerError) + return errMsg + } + + rawURL := fmt.Sprintf("http://%s/%s", svc.Spec.ClusterIP, filename) + seedImgURL, err := url.Parse(rawURL) + if err != nil { + errMsg := fmt.Errorf("failed to parse url '%s'", rawURL) + http.Error(resp, errMsg.Error(), http.StatusInternalServerError) + return errMsg + } + director := func(r *http.Request) { + r.URL = seedImgURL + } + + reverseProxy := &httputil.ReverseProxy{ + Director: director, + } + reverseProxy.ServeHTTP(resp, req) + + return nil +} + +func (i *InventoryServer) getManagedOSChangelog(osVersion string) (*elementalv1.ManagedOSChangelog, error) { + escapedToken := strings.Replace(osVersion, "\n", "", -1) + escapedToken = strings.Replace(escapedToken, "\r", "", -1) + + changelogList := &elementalv1.ManagedOSChangelogList{} + if err := i.List(i, changelogList); err != nil { + return nil, fmt.Errorf("failed to list managedoschangelogs") + } + + var changelog *elementalv1.ManagedOSChangelog + + for _, c := range changelogList.Items { + if path.Base(c.Spec.ManagedOSVersion) == escapedToken { + changelog = (&c).DeepCopy() + break + } + } + + if changelog == nil { + return nil, fmt.Errorf("failed to find changelog with ManagedOSVersion %s", escapedToken) + } + + for _, condition := range changelog.Status.Conditions { + if condition.Status != "True" { + return nil, fmt.Errorf("changelog %s/%s is not ready", changelog.Namespace, changelog.Name) + } + } + + return changelog, nil +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 578e0b17f..a8fa07549 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -87,7 +87,11 @@ func (i *InventoryServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) log.Errorf("seedimage download: %s", err.Error()) return } - + case "changelog": + if err := i.apiChangelog(resp, req, splittedPath); err != nil { + log.Errorf("changelog download: %s", err.Error()) + return + } default: log.Errorf("Unknown API: %s", api) http.Error(resp, fmt.Sprintf("unknwon api: %s", api), http.StatusBadRequest)