diff --git a/cli/cmd/migrations/base_migration.go b/cli/cmd/migrations/base_migration.go new file mode 100644 index 000000000..73e7221c8 --- /dev/null +++ b/cli/cmd/migrations/base_migration.go @@ -0,0 +1,8 @@ +package migrations + +type Migration interface { + Name() string // Unique name of the migration + Description() string // A brief description of the migration + TriggerVersion() string // The version at which the migration becomes applicable + Execute() error // Code to execute the migration +} diff --git a/cli/cmd/migrations/json-patch.go b/cli/cmd/migrations/json-patch.go deleted file mode 100644 index 23231b850..000000000 --- a/cli/cmd/migrations/json-patch.go +++ /dev/null @@ -1,30 +0,0 @@ -package migrations - -import "encoding/json" - -// when you want to update a k8s object, you have few options: -// 1. use the k8s client to get the object, update it and then save it back -// 2. use the k8s client patch method to apply just the changes you want -// 3. delete the object and create it from scratch -// -// The preferred way is to use the patch method, because it's clear what you're doing -// and it's more efficient than the other options. -// -// K8s also support multiple patch types. You can read more here: -// https://erosb.github.io/post/json-patch-vs-merge-patch/ -// This file includes types and helpers for the JSON-patch payload: -// https://datatracker.ietf.org/doc/html/rfc6902 - -type jsonPatchOperation struct { - Op string `json:"op"` // can be "add", "remove", "replace", "move", "copy", "test" - Path string `json:"path"` // required for all operations - Value string `json:"value,omitempty"` // required for "add", "replace" and "test" - From string `json:"from,omitempty"` // required for "move" and "copy -} - -type jsonPatchDocument []jsonPatchOperation - -func encodeJsonPatchDocument(patchDocument jsonPatchDocument) []byte { - data, _ := json.Marshal(patchDocument) - return data -} diff --git a/cli/cmd/migrations/manager.go b/cli/cmd/migrations/manager.go new file mode 100644 index 000000000..08df66190 --- /dev/null +++ b/cli/cmd/migrations/manager.go @@ -0,0 +1,52 @@ +package migrations + +import ( + "fmt" + + "github.com/odigos-io/odigos/cli/cmd/migrations/runtime_details_migration" + "github.com/odigos-io/odigos/cli/pkg/kube" + + "golang.org/x/mod/semver" +) + +type MigrationManager struct { + Migrations []Migration +} + +func NewMigrationManager(client *kube.Client) *MigrationManager { + return &MigrationManager{ + Migrations: []Migration{ + &runtime_details_migration.MigrateRuntimeDetails{Client: client}, + // Add more migrations here by referencing their structs + }, + } +} +func (m *MigrationManager) Run(fromVersion, toVersion string) error { + // Ensure versions are valid semantic versions + if !semver.IsValid(fromVersion) { + return fmt.Errorf("invalid from version: %s", fromVersion) + } + if !semver.IsValid(toVersion) { + return fmt.Errorf("invalid to version: %s", toVersion) + } + + for _, migration := range m.Migrations { + cutVersion := migration.TriggerVersion() + if !semver.IsValid(cutVersion) { + return fmt.Errorf("invalid cut version for migration %s: %s", migration.Name(), cutVersion) + } + + // Check if migration should be executed + if true { //semver.Compare(fromVersion, cutVersion) < 0 && semver.Compare(toVersion, cutVersion) >= 0 + fmt.Printf("Executing migration: %s - %s\n", migration.Name(), migration.Description()) + if err := migration.Execute(); err != nil { + return fmt.Errorf("migration %s failed: %w", migration.Name(), err) + } + fmt.Printf("Migration %s completed successfully.\n", migration.Name()) + } else { + fmt.Printf("Skipping migration: %s (cut version: %s, from: %s, to: %s)\n", + migration.Name(), cutVersion, fromVersion, toVersion) + } + } + return nil +} diff --git a/cli/cmd/migrations/runtime_details_migration/migrate_runtime_details.go b/cli/cmd/migrations/runtime_details_migration/migrate_runtime_details.go new file mode 100644 index 000000000..02597b754 --- /dev/null +++ b/cli/cmd/migrations/runtime_details_migration/migrate_runtime_details.go @@ -0,0 +1,159 @@ +package runtime_details_migration + +import ( + "context" + "fmt" + "strings" + + "github.com/odigos-io/odigos/api/odigos/v1alpha1" + "github.com/odigos-io/odigos/cli/pkg/kube" + "github.com/odigos-io/odigos/k8sutils/pkg/envoverwrite" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type MigrateRuntimeDetails struct { + Client *kube.Client +} + +func (m *MigrateRuntimeDetails) Name() string { + return "migrate-runtime-details" +} + +func (m *MigrateRuntimeDetails) Description() string { + return "Migrate old RuntimeDetailsByContainer structure to the new format" +} + +func (m *MigrateRuntimeDetails) TriggerVersion() string { + return "v1.0.139" +} + +func (m *MigrateRuntimeDetails) Execute() error { + fmt.Println("Migrating RuntimeDetailsByContainer....") + + gvr := schema.GroupVersionResource{Group: "odigos.io", Version: "v1alpha1", Resource: "instrumentationconfigs"} + + ctx := context.TODO() + instrumentationConfigs, err := m.Client.Dynamic.Resource(gvr).List(ctx, metav1.ListOptions{}) + if err != nil { + panic(fmt.Errorf("failed to list InstrumentationConfigs: %v", err)) + } + + workloadNamespaces := make(map[string]map[string][]string) // workloadType -> namespace -> []workloadNames + for _, item := range instrumentationConfigs.Items { + fmt.Printf("Found InstrumentationConfig: %s in namespace: %s\n", item.GetName(), item.GetNamespace()) + IcName := item.GetName() + IcNamespace := item.GetNamespace() + parts := strings.Split(IcName, "-") + if len(parts) < 2 { + fmt.Printf("Skipping invalid InstrumentationConfig name: %s\n", IcName) + continue + } + + workloadType := parts[0] // deployment/statefulset/aemonset + workloadName := strings.Join(parts[1:], "-") + if _, exists := workloadNamespaces[workloadType]; !exists { + workloadNamespaces[workloadType] = make(map[string][]string) + } + workloadNamespaces[workloadType][IcNamespace] = append(workloadNamespaces[workloadType][IcNamespace], workloadName) + } + for workloadType, namespaces := range workloadNamespaces { + switch workloadType { + case "deployment": + if err := fetchAndProcessDeployments(m.Client, namespaces); err != nil { + return err + } + case "statefulset": + if err := fetchAndProcessStatefulSets(m.Client, namespaces); err != nil { + return err + } + case "daemonset": + if err := fetchAndProcessDaemonSets(m.Client, namespaces); err != nil { + return err + } + default: + fmt.Printf("Unknown workload type: %s\n", workloadType) + } + } + + return nil +} + +func fetchAndProcessDeployments(clientset *kube.Client, namespaces map[string][]string) error { + for namespace, workloadNames := range namespaces { + fmt.Printf("Processing Deployments in namespace: %s\n", namespace) + deployments, err := clientset.AppsV1().Deployments(namespace).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("failed to list deployments in namespace %s: %v", namespace, err) + } + + for _, dep := range deployments.Items { + if contains(workloadNames, dep.Name) { + fmt.Printf("Processing Deployment: %s in namespace: %s\n", dep.Name, dep.Namespace) + originalEnvVar, _ := envoverwrite.NewOrigWorkloadEnvValues(dep.Annotations) + allNil := originalEnvVar.AreAllEnvValuesNil() + if allNil { + // update instrumentationConfig object + } + instrumentationConfig, err := clientset.OdigosClient.InstrumentationConfigs(dep.Namespace).Get(context.TODO(), dep.Name, metav1.GetOptions{}) + if err != nil { + fmt.Printf("Failed to get InstrumentationConfig: %v\n", err) + } + // TODO: Need to move the general operations to top level object and not per iteration. + // updating runtimeDetailsByContainer as Skipped - executed but nothing happen + for _, runtimeDetails := range instrumentationConfig.Status.RuntimeDetailsByContainer { + value := v1alpha1.ProcessingStateSkipped + runtimeDetails.RuntimeUpdateState = &value + } + for _, container := range dep.Spec.Template.Spec.Containers { + fmt.Printf("Processing container: %s\n", container.Name) + // Add your deployment-specific logic here + } + } + } + } + return nil +} + +func fetchAndProcessStatefulSets(clientset *kube.Client, namespaces map[string][]string) error { + for namespace, workloadNames := range namespaces { + statefulSets, err := clientset.AppsV1().StatefulSets(namespace).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("failed to list statefulsets in namespace %s: %v", namespace, err) + } + + for _, sts := range statefulSets.Items { + if contains(workloadNames, sts.Name) { + fmt.Printf("Processing StatefulSet: %s in namespace: %s\n", sts.Name, sts.Namespace) + // Add your statefulset-specific logic here + } + } + } + return nil +} + +func fetchAndProcessDaemonSets(clientset *kube.Client, namespaces map[string][]string) error { + for namespace, workloadNames := range namespaces { + daemonSets, err := clientset.AppsV1().DaemonSets(namespace).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("failed to list daemonsets in namespace %s: %v", namespace, err) + } + + for _, ds := range daemonSets.Items { + if contains(workloadNames, ds.Name) { + fmt.Printf("Processing DaemonSet: %s in namespace: %s\n", ds.Name, ds.Namespace) + // Add your daemonset-specific logic here + } + } + } + return nil +} + +func contains(slice []string, item string) bool { + for _, v := range slice { + if v == item { + return true + } + } + return false +} diff --git a/cli/cmd/resources/applyresources.go b/cli/cmd/resources/applyresources.go index 1edcdac01..c4e2d6b31 100644 --- a/cli/cmd/resources/applyresources.go +++ b/cli/cmd/resources/applyresources.go @@ -58,6 +58,5 @@ func GetCurrentConfig(ctx context.Context, client *kube.Client, ns string) (*com } func GetDeprecatedConfig(ctx context.Context, client *kube.Client, ns string) (*v1alpha1.OdigosConfiguration, error) { - return client.OdigosClient.OdigosConfigurations(ns).Get(ctx, consts.OdigosConfigurationName, metav1.GetOptions{}) + return client.OdigosClient.OdigosConfigurations(ns).Get(ctx, consts.OdigosConfigurationName, metav1.GetOptions{}) } - diff --git a/cli/cmd/upgrade.go b/cli/cmd/upgrade.go index 11331a78f..735abfa0b 100644 --- a/cli/cmd/upgrade.go +++ b/cli/cmd/upgrade.go @@ -8,6 +8,7 @@ import ( "os" "github.com/hashicorp/go-version" + "github.com/odigos-io/odigos/cli/cmd/migrations" "github.com/odigos-io/odigos/cli/cmd/resources" "github.com/odigos-io/odigos/cli/cmd/resources/odigospro" cmdcontext "github.com/odigos-io/odigos/cli/pkg/cmd_context" @@ -41,6 +42,7 @@ and apply any required migrations and adaptations.`, var operation string + var currOdigosVersion string skipVersionCheckFlag := cmd.Flag("skip-version-check") if skipVersionCheckFlag == nil || !cmd.Flag("skip-version-check").Changed { @@ -50,7 +52,7 @@ and apply any required migrations and adaptations.`, os.Exit(1) } - currOdigosVersion := cm.Data["ODIGOS_VERSION"] + currOdigosVersion = cm.Data["ODIGOS_VERSION"] if currOdigosVersion == "" { fmt.Println("Odigos upgrade failed - unable to read the current Odigos version for migration") os.Exit(1) @@ -126,6 +128,12 @@ and apply any required migrations and adaptations.`, fmt.Println("Odigos upgrade failed - unable to cleanup old Odigos resources.") os.Exit(1) } + manager := migrations.NewMigrationManager(client) + // Run migrations + if err := manager.Run(currOdigosVersion, versionFlag); err != nil { + fmt.Printf("Upgrade failed: %v\n", err) + return + } }, } diff --git a/cli/go.mod b/cli/go.mod index 72073c3c8..295ad5d3a 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -59,6 +59,7 @@ require ( go.opentelemetry.io/otel v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect go.uber.org/multierr v1.11.0 + golang.org/x/mod v0.22.0 golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sys v0.26.0 // indirect diff --git a/cli/go.sum b/cli/go.sum index 341f2ee71..a3d404283 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -130,6 +130,8 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= diff --git a/k8sutils/pkg/envoverwrite/origenv.go b/k8sutils/pkg/envoverwrite/origenv.go index 32369d13f..ff7d4f703 100644 --- a/k8sutils/pkg/envoverwrite/origenv.go +++ b/k8sutils/pkg/envoverwrite/origenv.go @@ -12,7 +12,7 @@ import ( // original manifest values for the env vars of a workload // This is specific to k8s as it assumes there is OriginalEnv per container type OrigWorkloadEnvValues struct { - origManifestValues map[string]envOverwrite.OriginalEnv + origManifestValues map[string]envOverwrite.OriginalEnv //container name -> env name -> original value modifiedSinceCreated bool } @@ -108,3 +108,16 @@ func (o *OrigWorkloadEnvValues) DeleteFromObj(obj metav1.Object) bool { delete(currentAnnotations, consts.ManifestEnvOriginalValAnnotation) return true } + +// AreAllEnvValuesNil iterates over all containers in origManifestValues +// and checks if all their keys' values are nil. Returns true if all values are nil. +func (o *OrigWorkloadEnvValues) AreAllEnvValuesNil() bool { + for _, envMap := range o.origManifestValues { + for _, originalValue := range envMap { + if originalValue != nil { + return false + } + } + } + return true +}