From 8981b5b4117661b342e1fbf69d6751122e913a5f Mon Sep 17 00:00:00 2001 From: RealAnna <89971034+RealAnna@users.noreply.github.com> Date: Tue, 6 Feb 2024 14:47:08 +0100 Subject: [PATCH] chore: add KeptnApp migration script (#2959) Co-authored-by: Meg McRoberts Signed-off-by: vickysomtee --- docs/docs/migrate/keptnapp/index.md | 16 +++ lifecycle-operator/Makefile | 6 +- lifecycle-operator/converter/convert_app.go | 112 ++++++++++++++++++ .../converter/convert_app_test.go | 109 +++++++++++++++++ .../converter/example_keptnapp.yaml | 18 +++ .../converter/example_output.yaml | 32 +++++ 6 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 lifecycle-operator/converter/convert_app.go create mode 100644 lifecycle-operator/converter/convert_app_test.go create mode 100644 lifecycle-operator/converter/example_keptnapp.yaml create mode 100644 lifecycle-operator/converter/example_output.yaml diff --git a/docs/docs/migrate/keptnapp/index.md b/docs/docs/migrate/keptnapp/index.md index 8e2096c1eca..d88e26b48a6 100644 --- a/docs/docs/migrate/keptnapp/index.md +++ b/docs/docs/migrate/keptnapp/index.md @@ -36,6 +36,22 @@ introduced in the `v1beta1` API version: 3. Add the `app.kubernetes.io/managed-by: keptn` annotation to the `KeptnApp` resource if it is not already there. +You can migrate your KeptnApp manually or, if you have +[go](https://go.dev/) +installed +on your machine, use the script provided +[here](https://github.com/keptn/lifecycle-toolkit/tree/main/lifecycle-operator/converter). + +```bash + go run convert_app.go path_to_keptnapp_to_convert path_to_desired_output_file +``` + +For instance, to run the example file conversion, the command is: + +```bash + go run convert_app.go example_keptnapp.yaml example_output.yaml +``` + > **Note** Be sure that all of your application resources (such as diff --git a/lifecycle-operator/Makefile b/lifecycle-operator/Makefile index 3738ddf60fa..4231e00d88b 100644 --- a/lifecycle-operator/Makefile +++ b/lifecycle-operator/Makefile @@ -88,13 +88,15 @@ unit-test: manifests fmt vet generate envtest ## Run tests. go test ./apis/... -v -coverprofile cover-apis.out go test ./controllers/... -v -coverprofile cover-pkg.out go test ./webhooks/... -v -coverprofile cover-main.out + go test ./converter -v -coverprofile cover-converter.out sed -i '/mode: set/d' "cover-cmd.out" sed -i '/mode: set/d' "cover-apis.out" sed -i '/mode: set/d' "cover-pkg.out" sed -i '/mode: set/d' "cover-main.out" + sed -i '/mode: set/d' "cover-converter.out" echo "mode: set" > cover.out - cat cover-cmd.out cover-main.out cover-pkg.out cover-apis.out >> cover.out - rm cover-cmd.out cover-pkg.out cover-main.out cover-apis.out + cat cover-converter.out cover-cmd.out cover-main.out cover-pkg.out cover-apis.out >> cover.out + rm cover-converter.out cover-cmd.out cover-pkg.out cover-main.out cover-apis.out .PHONY: component-test component-test: manifests generate envtest ## Run tests. diff --git a/lifecycle-operator/converter/convert_app.go b/lifecycle-operator/converter/convert_app.go new file mode 100644 index 00000000000..1f0f6096703 --- /dev/null +++ b/lifecycle-operator/converter/convert_app.go @@ -0,0 +1,112 @@ +package main + +import ( + "fmt" + "log" + "os" + + klcv1alpha3 "github.com/keptn/lifecycle-toolkit/lifecycle-operator/apis/lifecycle/v1alpha3" + klcv1beta1 "github.com/keptn/lifecycle-toolkit/lifecycle-operator/apis/lifecycle/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" +) + +const keptnAnnotation = "app.kubernetes.io/managed-by" +const keptn = "keptn" + +func main() { + if len(os.Args) != 3 { + fmt.Println("Usage: go run main.go app_cr_input_file.yaml output_file.yaml") + os.Exit(1) + } + + inputFile := os.Args[1] + outputFile := os.Args[2] + + if err := transformKeptnApp(inputFile, outputFile); err != nil { + log.Fatalf("Error transforming KeptnApp: %v", err) + } + + fmt.Println("Transformation completed. Output written to", outputFile) +} + +func transformKeptnApp(inputFile, outputFile string) error { + inputContent, err := os.ReadFile(inputFile) + if err != nil { + return fmt.Errorf("error reading input file: %w", err) + } + + keptnAppV1beta1, keptnAppContext, err := parseAndTransform(inputContent) + if err != nil { + return err + } + + outputContent := combineYAML(keptnAppV1beta1, keptnAppContext) + if err := os.WriteFile(outputFile, []byte(outputContent), 0644); err != nil { + return fmt.Errorf("error writing to output file: %w", err) + } + return nil +} + +func parseAndTransform(inputContent []byte) ([]byte, []byte, error) { + var keptnApp klcv1alpha3.KeptnApp + if err := yaml.Unmarshal(inputContent, &keptnApp); err != nil { + return nil, nil, fmt.Errorf("error unmarshalling KeptnApp: %w", err) + } + + var keptnAppV1beta1 klcv1beta1.KeptnApp + if err := yaml.Unmarshal(inputContent, &keptnAppV1beta1); err != nil { + return nil, nil, fmt.Errorf("error unmarshalling KeptnAppV1beta1: %w", err) + } + + addKeptnAnnotation(&keptnAppV1beta1.ObjectMeta) + keptnAppV1beta1.TypeMeta.APIVersion = "lifecycle.keptn.sh/v1beta1" + + keptnAppContext := transformKeptnAppContext(keptnApp) + + keptnAppV1beta1YAML, err := yaml.Marshal(keptnAppV1beta1) + if err != nil { + return nil, nil, fmt.Errorf("error marshalling KeptnAppV1beta1 to YAML: %w", err) + } + + keptnAppContextYAML, err := yaml.Marshal(keptnAppContext) + if err != nil { + return nil, nil, fmt.Errorf("error marshalling KeptnAppContext to YAML: %w", err) + } + + return keptnAppV1beta1YAML, keptnAppContextYAML, nil +} + +func transformKeptnAppContext(keptnApp klcv1alpha3.KeptnApp) klcv1beta1.KeptnAppContext { + return klcv1beta1.KeptnAppContext{ + TypeMeta: metav1.TypeMeta{ + Kind: "KeptnAppContext", + APIVersion: "lifecycle.keptn.sh/v1beta1", + }, + + ObjectMeta: metav1.ObjectMeta{ + Name: keptnApp.Name, + Namespace: keptnApp.Namespace, + }, + Spec: klcv1beta1.KeptnAppContextSpec{ + DeploymentTaskSpec: klcv1beta1.DeploymentTaskSpec{ + PreDeploymentTasks: keptnApp.Spec.PreDeploymentTasks, + PreDeploymentEvaluations: keptnApp.Spec.PreDeploymentEvaluations, + PostDeploymentTasks: keptnApp.Spec.PostDeploymentTasks, + PostDeploymentEvaluations: keptnApp.Spec.PostDeploymentEvaluations, + }, + }, + } +} + +func combineYAML(keptnAppV1beta1YAML, keptnAppContextYAML []byte) string { + return fmt.Sprintf("%s\n---\n%s", string(keptnAppV1beta1YAML), string(keptnAppContextYAML)) +} + +func addKeptnAnnotation(resource *metav1.ObjectMeta) { + annotations := resource.GetAnnotations() + if annotations == nil { + resource.Annotations = make(map[string]string, 1) + } + resource.Annotations[keptnAnnotation] = keptn +} diff --git a/lifecycle-operator/converter/convert_app_test.go b/lifecycle-operator/converter/convert_app_test.go new file mode 100644 index 00000000000..c064139b4b2 --- /dev/null +++ b/lifecycle-operator/converter/convert_app_test.go @@ -0,0 +1,109 @@ +package main + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const inputFileName = "example_keptnapp.yaml" +const outputFileName = "example_output.yaml" + +func TestMigration(t *testing.T) { + // Set up a temporary directory for test files + // Run the main function with test arguments + inputFile := inputFileName + outputFile := outputFileName + os.Args = []string{"main", inputFile, outputFile} + main() + + // Read the expected output file content + expectedOutput, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("Error reading expected output file: %v", err) + } + + // Read the actual output file content + actualOutput, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("Error reading actual output file: %v", err) + } + + // Assert that the actual output file content matches the expected output + if string(actualOutput) != string(expectedOutput) { + t.Errorf("Unexpected output content. Expected:\n%s\n\nActual:\n%s", string(expectedOutput), string(actualOutput)) + } + +} + +func TestAddKeptnAnnotation(t *testing.T) { + // Test case 1: Annotations map is nil + t.Run("AnnotationsMapIsNil", func(t *testing.T) { + resource := &metav1.ObjectMeta{} + addKeptnAnnotation(resource) + + // Check if the annotation was added + if value, exists := resource.Annotations[keptnAnnotation]; !exists || value != keptn { + t.Errorf("Annotation not added correctly. Expected: %s=%s, Actual: %s=%s", keptnAnnotation, keptn, keptnAnnotation, value) + } + }) + + // Test case 2: Annotations map is not nil + t.Run("AnnotationsMapIsNotNil", func(t *testing.T) { + // Existing annotations + existingAnnotations := map[string]string{ + "existing-key": "existing-value", + } + + resource := &metav1.ObjectMeta{ + Annotations: existingAnnotations, + } + + addKeptnAnnotation(resource) + + // Check if the annotation was added + if value, exists := resource.Annotations[keptnAnnotation]; !exists || value != keptn { + t.Errorf("Annotation not added correctly. Expected: %s=%s, Actual: %s=%s", keptnAnnotation, keptn, keptnAnnotation, value) + } + + // Check if existing annotations are preserved + for key, value := range existingAnnotations { + if resource.Annotations[key] != value { + t.Errorf("Existing annotation %s=%s is not preserved", key, value) + } + } + }) +} + +func TestParseAndTransformBadYAML(t *testing.T) { + invalidYAML := []byte("This is not valid YAML") + // Attempt to parse and transform the invalid app YAML + _, _, err := parseAndTransform(invalidYAML) + if err == nil { + t.Error("Expected an error but got nil") + } +} + +func TestTransformKeptnApp_UnmarshalFailure(t *testing.T) { + tmpDir := t.TempDir() + + // Create a temporary file with invalid YAML content + invalidYAML := []byte("This is not valid YAML") + err := os.WriteFile(tmpDir+"/validfile.yaml", invalidYAML, 0644) + require.NoError(t, err) + + // Attempt to transform the invalid YAML file + err = transformKeptnApp(tmpDir+"/validfile.yaml", tmpDir+"/output.yaml") + + require.Error(t, err) +} + +func TestTransformKeptnApp_ReadFileFailure(t *testing.T) { + //unexisting file + tmpDir := t.TempDir() + err := transformKeptnApp(tmpDir+"/validfile.yaml", tmpDir+"/output.yaml") + require.Error(t, err) + t.Log(err) +} diff --git a/lifecycle-operator/converter/example_keptnapp.yaml b/lifecycle-operator/converter/example_keptnapp.yaml new file mode 100644 index 00000000000..a66e6c4469c --- /dev/null +++ b/lifecycle-operator/converter/example_keptnapp.yaml @@ -0,0 +1,18 @@ +apiVersion: lifecycle.keptn.sh/v1alpha3 +kind: KeptnApp +metadata: + name: "some-keptn-app" + namespace: "my-app-ns" +spec: + version: "1.2.3" + workloads: + - name: podtato-head-left-arm + version: 0.2.7 + preDeploymentTasks: + - pre-deployment-task + preDeploymentEvaluations: + - pre-deployment-evaluation + postDeploymentTasks: + - post-deployment-task + postDeploymentEvaluations: + - post-deployment-evaluation diff --git a/lifecycle-operator/converter/example_output.yaml b/lifecycle-operator/converter/example_output.yaml new file mode 100644 index 00000000000..41607eae604 --- /dev/null +++ b/lifecycle-operator/converter/example_output.yaml @@ -0,0 +1,32 @@ +apiVersion: lifecycle.keptn.sh/v1beta1 +kind: KeptnApp +metadata: + annotations: + app.kubernetes.io/managed-by: keptn + creationTimestamp: null + name: some-keptn-app + namespace: my-app-ns +spec: + version: 1.2.3 + workloads: + - name: podtato-head-left-arm + version: 0.2.7 +status: {} + +--- +apiVersion: lifecycle.keptn.sh/v1beta1 +kind: KeptnAppContext +metadata: + creationTimestamp: null + name: some-keptn-app + namespace: my-app-ns +spec: + postDeploymentEvaluations: + - post-deployment-evaluation + postDeploymentTasks: + - post-deployment-task + preDeploymentEvaluations: + - pre-deployment-evaluation + preDeploymentTasks: + - pre-deployment-task +status: {}