From e47062d64cf635f5c8658fecd23648f167081b3a Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Thu, 30 Jan 2025 19:18:53 +0530 Subject: [PATCH 01/33] add file sanitization helpers Signed-off-by: aabidsofi19 --- files/sanitization.go | 156 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 files/sanitization.go diff --git a/files/sanitization.go b/files/sanitization.go new file mode 100644 index 00000000..c2ae397d --- /dev/null +++ b/files/sanitization.go @@ -0,0 +1,156 @@ +package files + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "encoding/json" + // "errors" + "fmt" + "io" + // "io/ioutil" + // "mime/multipart" + "os" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" +) + +type SanitizedFile struct { + FileExt string + UnmarshalledFile interface{} + ExtractedContent *os.File +} + +func SanitizeFile(data []byte, fileName string, tempDir string) (SanitizedFile, error) { + + validExts := map[string]bool{ + ".json": true, + ".yml": true, + ".yaml": true, + ".tar": true, + ".tar.gz": true, + ".tgz": true, + ".zip": true, + } + + ext := filepath.Ext(fileName) + + // 1. Check if file has supported extension + if !validExts[ext] && !validExts[filepath.Ext(strings.TrimSuffix(fileName, ".gz"))] { + return SanitizedFile{}, fmt.Errorf("unsupported file extension: %s", ext) + } + + switch ext { + + case ".yml", ".yaml": + var unMarshalled interface{} + err := yaml.Unmarshal(data, &unMarshalled) + + if err != nil { + return SanitizedFile{}, err + } + + return SanitizedFile{ + UnmarshalledFile: unMarshalled, + FileExt: ext, + }, nil + + case ".json": + var unMarshalled interface{} + err := json.Unmarshal(data, &unMarshalled) + + if err != nil { + return SanitizedFile{}, err + } + + return SanitizedFile{ + UnmarshalledFile: unMarshalled, + FileExt: ext, + }, nil + + case ".tar", ".tar.gz", ".zip", ".gz": + + return SanitizeBundle(data, fileName, ext, tempDir) + + } + + return SanitizedFile{}, fmt.Errorf("Unsupported file format") + +} + +// will skip nested dirs right now +func SanitizeBundle(data []byte, fileName string, ext string, tempDir string) (SanitizedFile, error) { + + var tarReader *tar.Reader + input := bytes.NewReader(data) + + if strings.HasSuffix(fileName, ".gz") || strings.HasSuffix(fileName, ".tgz") { + gzReader, err := gzip.NewReader(input) + if err != nil { + return SanitizedFile{}, fmt.Errorf("failed to create gzip reader: %w", err) + } + defer gzReader.Close() + tarReader = tar.NewReader(gzReader) + } else { + tarReader = tar.NewReader(input) + } + + extractDir, _ := os.MkdirTemp(tempDir, fileName) + + // Extract and validate files in the bundle + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return SanitizedFile{}, fmt.Errorf("failed to read tar entry: %w", err) + } + + // Skip unsupported file types + ext := filepath.Ext(header.Name) + if ext != ".json" && ext != ".yaml" && ext != ".yml" { + continue + } + + // read the complete content of the file h.Name into the bs []byte + fileBytes, _ := io.ReadAll(tarReader) // Validate the extracted file + + if _, err := SanitizeFile(fileBytes, header.Name, tempDir); err != nil { + fmt.Printf("Skipping invalid file: %s\n", header.Name) + continue + } + + // Create a temporary file for the extracted content + tempFilePath := filepath.Join(extractDir, header.Name) + tempFile, err := os.Create(tempFilePath) + + // fail forward and continue + if err != nil { + fmt.Println("failed to create temp file: %w", err) + continue + } + defer tempFile.Close() + + // Write the extracted content to the temp file + if _, err := io.Copy(tempFile, tarReader); err != nil { + fmt.Println("failed to write to temp file: %w", err) + } + + } + + // Return a handle to the temp directory (or a specific file if needed) + extractedContent, err := os.Open(tempDir) + + if err == nil { + return SanitizedFile{ + FileExt: ext, + UnmarshalledFile: nil, + ExtractedContent: extractedContent, + }, nil + } + + return SanitizedFile{}, fmt.Errorf("Failed to open extracted dir %w", err) +} From 6668acce4a9e0a2b705f2a3d9d4988a667501c62 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Thu, 30 Jan 2025 19:19:10 +0530 Subject: [PATCH 02/33] add sanitization tests Signed-off-by: aabidsofi19 --- files/tests/sanitization_test.go | 119 +++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 files/tests/sanitization_test.go diff --git a/files/tests/sanitization_test.go b/files/tests/sanitization_test.go new file mode 100644 index 00000000..0e553eb3 --- /dev/null +++ b/files/tests/sanitization_test.go @@ -0,0 +1,119 @@ +package files_test + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/layer5io/meshkit/files" +) + +func TestSanitizeFile(t *testing.T) { + testCases := []struct { + name string + filePath string + expectedExt string + expectError bool + expectedErrMsg string + expectedContent map[string]interface{} + }{ + { + name: "Valid JSON", + filePath: "./samples/valid.json", + expectedExt: ".json", + expectedContent: map[string]interface{}{ + "hello": "world", + }, + }, + { + name: "Invalid JSON", + filePath: "./samples/invalid.json", + expectError: true, + }, + { + name: "Valid YAML", + filePath: "./samples/valid.yml", + expectedExt: ".yml", + expectedContent: map[string]interface{}{ + "hello": "world", + }, + }, + { + name: "Invalid YAML", + filePath: "./samples/invalid.yml", + expectError: true, + }, + { + name: "Unsupported extension", + filePath: "./samples/valid.txt", + expectError: true, + expectedErrMsg: fmt.Sprintf("unsupported file extension: %s", ".txt"), + }, + { + name: "Valid compressed extension", + filePath: "./samples/valid.tar.gz", + expectedExt: ".gz", + }, + { + name: "Empty file", + filePath: "./samples/empty.json", + expectError: true, + }, + { + name: "invalid tar.gz", + filePath: "./samples/invalid.tar.gz", + expectError: true, + }, + } + + for _, tc := range testCases { + + filename := filepath.Base(tc.filePath) + // Read file bytes + data, err := os.ReadFile(tc.filePath) + if err != nil { + t.Error("Error reading file:", err) + continue + } + + t.Run(tc.name, func(t *testing.T) { + result, err := files.SanitizeFile(data, filename, "./temp") + + if tc.expectError { + if err == nil { + t.Error("Expected error, got nil") + } else if tc.expectedErrMsg != "" && err.Error() != tc.expectedErrMsg { + t.Errorf("Expected error message %q, got %q", tc.expectedErrMsg, err.Error()) + } + return + } + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if result.FileExt != tc.expectedExt { + t.Errorf("Expected file extension %q, got %q", tc.expectedExt, result.FileExt) + } + + if tc.expectedContent != nil { + unmarshalled, ok := result.UnmarshalledFile.(map[string]interface{}) + if !ok { + t.Fatalf("Expected unmarshalled file to be a map, got %T", result.UnmarshalledFile) + } + + for key, expectedValue := range tc.expectedContent { + actualValue, exists := unmarshalled[key] + if !exists { + t.Errorf("Key %q not found in unmarshalled content", key) + continue + } + if actualValue != expectedValue { + t.Errorf("For key %q, expected value %v, got %v", key, expectedValue, actualValue) + } + } + } + }) + } +} From 2e29345c9d67bea4eadaecc2655aca412d11ba59 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Thu, 30 Jan 2025 19:19:26 +0530 Subject: [PATCH 03/33] add test samples Signed-off-by: aabidsofi19 --- files/tests/samples/empty.json | 0 files/tests/samples/empty.yml | 0 files/tests/samples/invalid.json | 1 + files/tests/samples/invalid.tar.gz | 0 files/tests/samples/invalid.yml | 11 +++++++++++ files/tests/samples/valid.json | 3 +++ files/tests/samples/valid.tar.gz | Bin 0 -> 212 bytes files/tests/samples/valid.txt | 1 + files/tests/samples/valid.yml | 1 + 9 files changed, 17 insertions(+) create mode 100644 files/tests/samples/empty.json create mode 100644 files/tests/samples/empty.yml create mode 100644 files/tests/samples/invalid.json create mode 100644 files/tests/samples/invalid.tar.gz create mode 100644 files/tests/samples/invalid.yml create mode 100644 files/tests/samples/valid.json create mode 100644 files/tests/samples/valid.tar.gz create mode 100644 files/tests/samples/valid.txt create mode 100644 files/tests/samples/valid.yml diff --git a/files/tests/samples/empty.json b/files/tests/samples/empty.json new file mode 100644 index 00000000..e69de29b diff --git a/files/tests/samples/empty.yml b/files/tests/samples/empty.yml new file mode 100644 index 00000000..e69de29b diff --git a/files/tests/samples/invalid.json b/files/tests/samples/invalid.json new file mode 100644 index 00000000..39a1b346 --- /dev/null +++ b/files/tests/samples/invalid.json @@ -0,0 +1 @@ +invalid json diff --git a/files/tests/samples/invalid.tar.gz b/files/tests/samples/invalid.tar.gz new file mode 100644 index 00000000..e69de29b diff --git a/files/tests/samples/invalid.yml b/files/tests/samples/invalid.yml new file mode 100644 index 00000000..c8b0b705 --- /dev/null +++ b/files/tests/samples/invalid.yml @@ -0,0 +1,11 @@ +name: "John Doe +age: 30 +skills: + - Python + - Go + - JavaScript +address: {street: "123 Main St", city: "New York", state: NY +hobbies: + - "Reading + - "Gaming" + - "Hiking diff --git a/files/tests/samples/valid.json b/files/tests/samples/valid.json new file mode 100644 index 00000000..6d7e5e13 --- /dev/null +++ b/files/tests/samples/valid.json @@ -0,0 +1,3 @@ +{ + "hello":"world" +} diff --git a/files/tests/samples/valid.tar.gz b/files/tests/samples/valid.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..8ec368c4c6242f6a06f4964564980221667012e5 GIT binary patch literal 212 zcmV;_04x6=iwFP!000001MSw`3W6{gfMJilis1#sHs=Q2jiHdS6ts+p?md5^&Wa9P zMDKGlCIdehFWTq6DptF$siVcnfi-4amCNL`SMz?RZKRAd*1A;L;knY%xJbz5MtVAS zhrAUc%JWTCygXO7U5J6gy2L^x+tT|cStqBa^+gh2<8M!eQ2MF=^WHD`j<@}*?9so@ z(xHE4ouU5_ru5fhDxd8CPgVDe{{ei^zsa;4zyC%``VZlL|6;s<`Ue02000000001h O-*p4S1#j>GC;$KvWNTIc literal 0 HcmV?d00001 diff --git a/files/tests/samples/valid.txt b/files/tests/samples/valid.txt new file mode 100644 index 00000000..557db03d --- /dev/null +++ b/files/tests/samples/valid.txt @@ -0,0 +1 @@ +Hello World diff --git a/files/tests/samples/valid.yml b/files/tests/samples/valid.yml new file mode 100644 index 00000000..bb56b055 --- /dev/null +++ b/files/tests/samples/valid.yml @@ -0,0 +1 @@ +hello: world From 27d8c6760ff9fc8f97eab99db06e94c15d5e21c6 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Thu, 30 Jan 2025 20:54:49 +0530 Subject: [PATCH 04/33] add support for extracting nested tar Signed-off-by: aabidsofi19 --- files/sanitization.go | 58 +++++++++++++++++------------- files/tests/samples/nested.tar.gz | Bin 0 -> 792 bytes files/tests/sanitization_test.go | 5 +++ 3 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 files/tests/samples/nested.tar.gz diff --git a/files/sanitization.go b/files/sanitization.go index c2ae397d..a5634491 100644 --- a/files/sanitization.go +++ b/files/sanitization.go @@ -76,7 +76,7 @@ func SanitizeFile(data []byte, fileName string, tempDir string) (SanitizedFile, } - return SanitizedFile{}, fmt.Errorf("Unsupported file format") + return SanitizedFile{}, fmt.Errorf("Unsupported file extension %s", ext) } @@ -109,34 +109,44 @@ func SanitizeBundle(data []byte, fileName string, ext string, tempDir string) (S return SanitizedFile{}, fmt.Errorf("failed to read tar entry: %w", err) } - // Skip unsupported file types - ext := filepath.Ext(header.Name) - if ext != ".json" && ext != ".yaml" && ext != ".yml" { - continue - } + switch header.Typeflag { - // read the complete content of the file h.Name into the bs []byte - fileBytes, _ := io.ReadAll(tarReader) // Validate the extracted file + case tar.TypeDir: // If it's a directory, create it - if _, err := SanitizeFile(fileBytes, header.Name, tempDir); err != nil { - fmt.Printf("Skipping invalid file: %s\n", header.Name) - continue - } + target := filepath.Join(extractDir, header.Name) + if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil { + return SanitizedFile{}, err + } - // Create a temporary file for the extracted content - tempFilePath := filepath.Join(extractDir, header.Name) - tempFile, err := os.Create(tempFilePath) + case tar.TypeReg: + ext := filepath.Ext(header.Name) + if ext != ".json" && ext != ".yaml" && ext != ".yml" { + continue + } - // fail forward and continue - if err != nil { - fmt.Println("failed to create temp file: %w", err) - continue - } - defer tempFile.Close() + // read the complete content of the file h.Name into the bs []byte + fileBytes, _ := io.ReadAll(tarReader) // Validate the extracted file + + if _, err := SanitizeFile(fileBytes, header.Name, tempDir); err != nil { + fmt.Printf("Skipping invalid file: %s\n", header.Name) + continue + } + + // Create a temporary file for the extracted content + tempFilePath := filepath.Join(extractDir, header.Name) + tempFile, err := os.Create(tempFilePath) + + // fail forward and continue + if err != nil { + fmt.Println("failed to create temp file: %w", err) + continue + } + defer tempFile.Close() - // Write the extracted content to the temp file - if _, err := io.Copy(tempFile, tarReader); err != nil { - fmt.Println("failed to write to temp file: %w", err) + // Write the extracted content to the temp file + if _, err := io.Copy(tempFile, tarReader); err != nil { + fmt.Println("failed to write to temp file: %w", err) + } } } diff --git a/files/tests/samples/nested.tar.gz b/files/tests/samples/nested.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..070cd9e5e17e8565f5b7d74656e711a85ba9c884 GIT binary patch literal 792 zcmV+z1Lyo7iwFP!0000011nCD2BpqKx&XaNw6e?xfrZ(?d> z4vc>j1EBP1{L=wI<6Qw1@1y1G$OEwa56S(&te}@(MQu9><$q%%6L|m6&;Zo`Gd4FF zE&u5PFIc9TfgXB7hjCa`(h(C zW_tM@Za&<3>Cz656Hnc$tjgN= zh$@}>>>IkrCVX3LN8_PgN1Rk{U;3>UslI&KS689<$NB!P0l!xM|9nsXN8k3ptKVGQ zf9}%4`qa4#{|hYpay?0F{+s{)q2IgzGuB+#e{zfT{{L=XzxW^4{ZHCc|BD^&kB0lD z%!gU?KQL$vP(Th*fL{O8q68qJ{RbWYF)=bX7|s9m0I22v0FD0u;~X0Q#)f7l;PD?r zAIEM!_f;82|u Date: Thu, 30 Jan 2025 23:39:21 +0530 Subject: [PATCH 05/33] return raw data from after sanitization Signed-off-by: aabidsofi19 --- files/sanitization.go | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/files/sanitization.go b/files/sanitization.go index a5634491..5a8c4800 100644 --- a/files/sanitization.go +++ b/files/sanitization.go @@ -18,8 +18,9 @@ import ( ) type SanitizedFile struct { - FileExt string - UnmarshalledFile interface{} + FileExt string + RawData []byte + // incase of bundle like tar ExtractedContent *os.File } @@ -45,29 +46,26 @@ func SanitizeFile(data []byte, fileName string, tempDir string) (SanitizedFile, switch ext { case ".yml", ".yaml": - var unMarshalled interface{} - err := yaml.Unmarshal(data, &unMarshalled) - + err := IsValidYaml(data) if err != nil { - return SanitizedFile{}, err + return SanitizedFile{}, fmt.Errorf("File is not valid yaml: %w", err) } return SanitizedFile{ - UnmarshalledFile: unMarshalled, - FileExt: ext, + FileExt: ext, + RawData: data, }, nil case ".json": - var unMarshalled interface{} - err := json.Unmarshal(data, &unMarshalled) + err := IsValidJson(data) if err != nil { - return SanitizedFile{}, err + return SanitizedFile{}, fmt.Errorf("File is not valid json: %w", err) } return SanitizedFile{ - UnmarshalledFile: unMarshalled, - FileExt: ext, + FileExt: ext, + RawData: data, }, nil case ".tar", ".tar.gz", ".zip", ".gz": @@ -97,6 +95,7 @@ func SanitizeBundle(data []byte, fileName string, ext string, tempDir string) (S tarReader = tar.NewReader(input) } + // cleaning up is resposibility of the tempDir owner extractDir, _ := os.MkdirTemp(tempDir, fileName) // Extract and validate files in the bundle @@ -157,10 +156,20 @@ func SanitizeBundle(data []byte, fileName string, ext string, tempDir string) (S if err == nil { return SanitizedFile{ FileExt: ext, - UnmarshalledFile: nil, + RawData: data, ExtractedContent: extractedContent, }, nil } return SanitizedFile{}, fmt.Errorf("Failed to open extracted dir %w", err) } + +func IsValidYaml(data []byte) error { + var unMarshalled interface{} + return yaml.Unmarshal(data, &unMarshalled) +} + +func IsValidJson(data []byte) error { + var unMarshalled interface{} + return json.Unmarshal(data, &unMarshalled) +} From 347136dc6e61750caf1cef632d64e6ee4b312006 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Thu, 30 Jan 2025 23:39:37 +0530 Subject: [PATCH 06/33] add support for identifying designs Signed-off-by: aabidsofi19 --- files/identification.go | 65 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 files/identification.go diff --git a/files/identification.go b/files/identification.go new file mode 100644 index 00000000..706c1b84 --- /dev/null +++ b/files/identification.go @@ -0,0 +1,65 @@ +package files + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/meshery/schemas/models/v1beta1/pattern" + "gopkg.in/yaml.v3" +) + +const MESHERY_DESIGN = "Design" +const KUBERNETES_MANIFEST = "KubernetesManifest" +const DOCKER_COMPOSE = "DockerCompose" +const KUSTOMIZATION = "Kustomization" +const HELM_CHART = "HelmChart" +const MESHERY_VIEW = "View" + +type IdentifiedFile struct { + Type string + // pattern.PatternFile,etc + ParsedFile interface{} +} + +// func IdentifyFileType(sanitizedFile:SanitizedFile) (string,error){ + +// } + +func IdentifyFile(sanitizedFile SanitizedFile) (IdentifiedFile, error) { + if parsed, err := ParseFileAsMesheryDesign(sanitizedFile); err == nil { + fmt.Println("parsed") + return IdentifiedFile{ + Type: MESHERY_DESIGN, + ParsedFile: parsed, + }, nil + } + + return IdentifiedFile{}, fmt.Errorf("Unsupported FileType") +} + +func ParseFileAsMesheryDesign(file SanitizedFile) (pattern.PatternFile, error) { + + var parsed pattern.PatternFile + + switch file.FileExt { + + case ".yml", ".yaml": + + decoder := yaml.NewDecoder(bytes.NewReader(file.RawData)) + decoder.KnownFields(true) + err := decoder.Decode(&parsed) + return parsed, err + + case ".json": + + decoder := json.NewDecoder(bytes.NewReader(file.RawData)) + decoder.DisallowUnknownFields() + err := decoder.Decode(&parsed) + return parsed, err + + default: + return pattern.PatternFile{}, fmt.Errorf("Invalid File extension %s", file.FileExt) + } + +} From d8c2833af17a06ac59c831f28f1f32142bf8393f Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Thu, 30 Jan 2025 23:39:54 +0530 Subject: [PATCH 07/33] update tests Signed-off-by: aabidsofi19 --- files/tests/sanitization_test.go | 34 +++++++++++++------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/files/tests/sanitization_test.go b/files/tests/sanitization_test.go index 7296fb2f..25051a36 100644 --- a/files/tests/sanitization_test.go +++ b/files/tests/sanitization_test.go @@ -17,14 +17,12 @@ func TestSanitizeFile(t *testing.T) { expectError bool expectedErrMsg string expectedContent map[string]interface{} + expectedType string }{ { name: "Valid JSON", filePath: "./samples/valid.json", expectedExt: ".json", - expectedContent: map[string]interface{}{ - "hello": "world", - }, }, { name: "Invalid JSON", @@ -35,9 +33,6 @@ func TestSanitizeFile(t *testing.T) { name: "Valid YAML", filePath: "./samples/valid.yml", expectedExt: ".yml", - expectedContent: map[string]interface{}{ - "hello": "world", - }, }, { name: "Invalid YAML", @@ -70,6 +65,13 @@ func TestSanitizeFile(t *testing.T) { filePath: "./samples/invalid.tar.gz", expectError: true, }, + + { + name: "Can Identify Design", + filePath: "./samples/valid_design.yml", + expectedExt: ".yml", + expectedType: files.MESHERY_DESIGN, + }, } for _, tc := range testCases { @@ -102,23 +104,15 @@ func TestSanitizeFile(t *testing.T) { t.Errorf("Expected file extension %q, got %q", tc.expectedExt, result.FileExt) } - if tc.expectedContent != nil { - unmarshalled, ok := result.UnmarshalledFile.(map[string]interface{}) - if !ok { - t.Fatalf("Expected unmarshalled file to be a map, got %T", result.UnmarshalledFile) - } + if tc.expectedType != "" { + identified_file, err := files.IdentifyFile(result) - for key, expectedValue := range tc.expectedContent { - actualValue, exists := unmarshalled[key] - if !exists { - t.Errorf("Key %q not found in unmarshalled content", key) - continue - } - if actualValue != expectedValue { - t.Errorf("For key %q, expected value %v, got %v", key, expectedValue, actualValue) - } + if (err != nil || identified_file.Type != tc.expectedType) && !tc.expectError { + t.Errorf("Failed To Identify File as %s , got %s", tc.expectedType, identified_file.Type) } + } + }) } } From 802bedb29ae130026c72381f7506248f37060538 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Thu, 30 Jan 2025 23:40:33 +0530 Subject: [PATCH 08/33] update samples Signed-off-by: aabidsofi19 --- files/tests/samples/valid_design.yml | 396 +++++++++++ files/tests/samples/valid_manifest.yml | 885 +++++++++++++++++++++++++ 2 files changed, 1281 insertions(+) create mode 100644 files/tests/samples/valid_design.yml create mode 100644 files/tests/samples/valid_manifest.yml diff --git a/files/tests/samples/valid_design.yml b/files/tests/samples/valid_design.yml new file mode 100644 index 00000000..da13e8d0 --- /dev/null +++ b/files/tests/samples/valid_design.yml @@ -0,0 +1,396 @@ +id: 00000000-0000-0000-0000-000000000000 +components: +- capabilities: [] + component: + kind: ConfigMap + schema: "" + version: v1 + configuration: + metadata: + namespace: default + description: "" + displayName: config-map-jp + format: JSON + id: 3a7cbdf2-110b-4334-b3e3-a91ec4319bb7 + metadata: + genealogy: "" + isAnnotation: false + isNamespaced: true + published: false + instanceDetails: null + additionalproperties: + source_uri: git://github.com/kubernetes/kubernetes/master/api/openapi-spec/v3 + model: + category: + id: 00000000-0000-0000-0000-000000000000 + name: Orchestration & Management + displayName: Kubernetes + id: bba54c44-a440-773a-a524-0114ebde5397 + metadata: + capabilities: null + isAnnotation: false + primaryColor: '#326CE5' + secondaryColor: '#7aa1f0' + svgColor: ui/public/static/img/meshmodels/kubernetes/color/kubernetes-color.svg + svgComplete: "" + svgWhite: ui/public/static/img/meshmodels/kubernetes/white/kubernetes-white.svg + model: + version: v1.32.0-alpha.3 + name: kubernetes + registrant: + created_at: 2025-01-27T22:09:21.794655001Z + credential_id: 00000000-0000-0000-0000-000000000000 + deleted_at: 0001-01-01T00:00:00Z + id: 9e04779c-94b6-6a03-575c-66f4c57541eb + kind: github + name: Github + status: registered + sub_type: "" + type: registry + updated_at: 2025-01-27T22:09:21.794655001Z + user_id: 00000000-0000-0000-0000-000000000000 + connection_id: 9e04779c-94b6-6a03-575c-66f4c57541eb + schemaVersion: models.meshery.io/v1beta1 + status: enabled + subCategory: Scheduling & Orchestration + version: v1.0.0 + components: null + relationships: null + components_count: 0 + relationships_count: 0 + schemaVersion: components.meshery.io/v1beta1 + status: enabled + styles: + background-opacity: 1 + body-text: "" + body-text-color: '#808080' + body-text-font-size: 12 + body-text-font-weight: "400" + body-text-horizontal-align: center + body-text-vertical-align: center + border-width: 0 + height: 24 + opacity: 1 + padding: 6 + position: + x: 738.4000244140625 + "y": 623 + primaryColor: '#326CE5' + secondaryColor: '#7aa1f0' + shape: bottom-round-rectangle + svgColor: ui/public/static/img/meshmodels/kubernetes/color/configmap-color.svg + svgWhite: ui/public/static/img/meshmodels/kubernetes/white/configmap-white.svg + width: 24 + z-index: 0 + version: v1.0.0 +- capabilities: + - description: Initiate a performance test. Meshery will execute the load generation, + collect metrics, and present the results. + displayName: Performance Test + entityState: + - instance + key: "" + kind: action + metadata: null + schemaVersion: capability.meshery.io/v1alpha1 + status: enabled + subType: perf-test + type: operator + version: 0.7.0 + - description: Configure the workload specific setting of a component + displayName: Workload Configuration + entityState: + - declaration + key: "" + kind: mutate + metadata: null + schemaVersion: capability.meshery.io/v1alpha1 + status: enabled + subType: config + type: configuration + version: 0.7.0 + - description: 'Configure Labels And Annotations for the component ' + displayName: Labels and Annotations Configuration + entityState: + - declaration + key: "" + kind: mutate + metadata: null + schemaVersion: capability.meshery.io/v1alpha1 + status: enabled + subType: labels-and-annotations + type: configuration + version: 0.7.0 + - description: View relationships for the component + displayName: Relationships + entityState: + - declaration + - instance + key: "" + kind: view + metadata: null + schemaVersion: capability.meshery.io/v1alpha1 + status: enabled + subType: relationship + type: configuration + version: 0.7.0 + - description: 'View Component Definition ' + displayName: Json Schema + entityState: + - declaration + - instance + key: "" + kind: view + metadata: null + schemaVersion: capability.meshery.io/v1alpha1 + status: enabled + subType: definition + type: configuration + version: 0.7.0 + - description: Configure the visual styles for the component + displayName: Styling + entityState: + - declaration + key: "" + kind: mutate + metadata: null + schemaVersion: capability.meshery.io/v1alpha1 + status: enabled + subType: "" + type: style + version: 0.7.0 + - description: Change the shape of the component + displayName: Change Shape + entityState: + - declaration + key: "" + kind: mutate + metadata: null + schemaVersion: capability.meshery.io/v1alpha1 + status: enabled + subType: shape + type: style + version: 0.7.0 + - description: Drag and Drop a component into a parent component in graph view + displayName: Compound Drag And Drop + entityState: + - declaration + key: "" + kind: interaction + metadata: null + schemaVersion: capability.meshery.io/v1alpha1 + status: enabled + subType: compoundDnd + type: graph + version: 0.7.0 + component: + kind: Namespace + schema: "" + version: v1 + configuration: {} + description: "" + displayName: default + format: JSON + id: 96821383-4f6c-4016-b4b6-438289274501 + metadata: + genealogy: parent + isAnnotation: false + isNamespaced: false + published: false + instanceDetails: null + additionalproperties: + source_uri: git://github.com/kubernetes/kubernetes/master/api/openapi-spec/v3 + model: + category: + id: 00000000-0000-0000-0000-000000000000 + name: Orchestration & Management + displayName: Kubernetes + id: bba54c44-a440-773a-a524-0114ebde5397 + metadata: + capabilities: null + isAnnotation: false + primaryColor: '#326CE5' + secondaryColor: '#7aa1f0' + svgColor: ui/public/static/img/meshmodels/kubernetes/color/kubernetes-color.svg + svgComplete: "" + svgWhite: ui/public/static/img/meshmodels/kubernetes/white/kubernetes-white.svg + model: + version: v1.32.0-alpha.3 + name: kubernetes + registrant: + created_at: 2025-01-27T22:09:21.794655001Z + credential_id: 00000000-0000-0000-0000-000000000000 + deleted_at: 0001-01-01T00:00:00Z + id: 9e04779c-94b6-6a03-575c-66f4c57541eb + kind: github + name: Github + status: registered + sub_type: "" + type: registry + updated_at: 2025-01-27T22:09:21.794655001Z + user_id: 00000000-0000-0000-0000-000000000000 + connection_id: 9e04779c-94b6-6a03-575c-66f4c57541eb + schemaVersion: models.meshery.io/v1beta1 + status: enabled + subCategory: Scheduling & Orchestration + version: v1.0.0 + components: null + relationships: null + components_count: 0 + relationships_count: 0 + schemaVersion: components.meshery.io/v1beta1 + status: enabled + styles: + background-image: none + background-opacity: 0.5 + border-style: dashed + border-width: 2 + primaryColor: '#326CE5' + secondaryColor: '#7aa1f0' + shape: rectangle + svgColor: ui/public/static/img/meshmodels/kubernetes/color/namespace-color.svg + svgComplete: ui/public/static/img/meshmodels/kubernetes/complete/namespace-complete.svg + svgWhite: ui/public/static/img/meshmodels/kubernetes/white/namespace-white.svg + version: v1.0.0 +name: Valid Design +metadata: {} +relationships: +- id: 5f557d98-2008-4969-bd77-29bf7fa94b02 + capabilities: null + evaluationQuery: "" + kind: hierarchical + metadata: + description: 'A hierarchical inventory relationship in which the configuration + of (parent) component is patched with the configuration of other (child) component. + Eg: The configuration of the EnvoyFilter (parent) component is patched with + the configuration as received from WASMFilter (child) component.' + styles: + primaryColor: "" + svgColor: "" + svgWhite: "" + isAnnotation: false + additionalproperties: {} + model: + category: + id: 00000000-0000-0000-0000-000000000000 + name: Orchestration & Management + displayName: Kubernetes + id: bba54c44-a440-773a-a524-0114ebde5397 + metadata: + capabilities: null + isAnnotation: false + primaryColor: '#326CE5' + secondaryColor: '#7aa1f0' + svgColor: ui/public/static/img/meshmodels/kubernetes/color/kubernetes-color.svg + svgComplete: "" + svgWhite: ui/public/static/img/meshmodels/kubernetes/white/kubernetes-white.svg + model: + version: v1.32.0-alpha.3 + name: kubernetes + registrant: + created_at: 0001-01-01T00:00:00Z + credential_id: 00000000-0000-0000-0000-000000000000 + deleted_at: 0001-01-01T00:00:00Z + id: 00000000-0000-0000-0000-000000000000 + kind: "" + name: "" + status: "" + sub_type: "" + type: "" + updated_at: 0001-01-01T00:00:00Z + user_id: 00000000-0000-0000-0000-000000000000 + connection_id: 9e04779c-94b6-6a03-575c-66f4c57541eb + schemaVersion: models.meshery.io/v1beta1 + status: enabled + subCategory: Scheduling & Orchestration + version: v1.0.0 + components: null + relationships: null + components_count: 0 + relationships_count: 0 + modelid: 00000000-0000-0000-0000-000000000000 + schemaVersion: relationships.meshery.io/v1alpha3 + selectors: + - allow: + from: + - id: 3a7cbdf2-110b-4334-b3e3-a91ec4319bb7 + kind: '*' + model: + category: + id: 00000000-0000-0000-0000-000000000000 + name: "" + displayName: "" + id: 00000000-0000-0000-0000-000000000000 + metadata: null + model: + version: "" + name: '*' + registrant: + created_at: 0001-01-01T00:00:00Z + credential_id: 00000000-0000-0000-0000-000000000000 + deleted_at: 0001-01-01T00:00:00Z + id: 00000000-0000-0000-0000-000000000000 + kind: "" + name: "" + status: "" + sub_type: "" + type: "" + updated_at: 0001-01-01T00:00:00Z + user_id: 00000000-0000-0000-0000-000000000000 + connection_id: 00000000-0000-0000-0000-000000000000 + schemaVersion: "" + status: "" + version: "" + components: null + relationships: null + components_count: 0 + relationships_count: 0 + patch: + patchStrategy: replace + mutatedRef: + - - configuration + - metadata + - namespace + to: + - id: 96821383-4f6c-4016-b4b6-438289274501 + kind: Namespace + model: + category: + id: 00000000-0000-0000-0000-000000000000 + name: "" + displayName: "" + id: 00000000-0000-0000-0000-000000000000 + metadata: null + model: + version: "" + name: kubernetes + registrant: + created_at: 0001-01-01T00:00:00Z + credential_id: 00000000-0000-0000-0000-000000000000 + deleted_at: 0001-01-01T00:00:00Z + id: 00000000-0000-0000-0000-000000000000 + kind: github + name: "" + status: "" + sub_type: "" + type: "" + updated_at: 0001-01-01T00:00:00Z + user_id: 00000000-0000-0000-0000-000000000000 + connection_id: 00000000-0000-0000-0000-000000000000 + schemaVersion: "" + status: "" + version: "" + components: null + relationships: null + components_count: 0 + relationships_count: 0 + patch: + patchStrategy: replace + mutatorRef: + - - displayName + subType: inventory + status: approved + type: parent + version: v1.0.0 +schemaVersion: designs.meshery.io/v1beta1 +version: 0.0.1 diff --git a/files/tests/samples/valid_manifest.yml b/files/tests/samples/valid_manifest.yml new file mode 100644 index 00000000..9a639905 --- /dev/null +++ b/files/tests/samples/valid_manifest.yml @@ -0,0 +1,885 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-app-mesh + name: meshery-app-mesh +spec: + replicas: 1 + selector: + match Labels: + io.kompose.service: meshery-app-mesh + strategy: {} + template: + metadata: + creation Timestamp: null + labels: + io.kompose.service: meshery-app-mesh + spec: + containers: + - image: layer5/meshery-app-mesh:stable-latest + image Pull Policy: Always + name: meshery-app-mesh + ports: + - container Port: 10005 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-app-mesh + name: meshery-app-mesh +spec: + ports: + - name: "10005" + port: 10005 + target Port: 10005 + selector: + io.kompose.service: meshery-app-mesh +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-cilium + name: meshery-cilium +spec: + replicas: 1 + selector: + match Labels: + io.kompose.service: meshery-cilium + strategy: {} + template: + metadata: + creation Timestamp: null + labels: + io.kompose.service: meshery-cilium + spec: + containers: + - image: layer5/meshery-cilium:stable-latest + image Pull Policy: Always + name: meshery-cilium + ports: + - container Port: 10012 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-cilium + name: meshery-cilium +spec: + ports: + - name: "10012" + port: 10012 + target Port: 10012 + selector: + io.kompose.service: meshery-cilium +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-consul + name: meshery-consul +spec: + replicas: 1 + selector: + match Labels: + io.kompose.service: meshery-consul + strategy: {} + template: + metadata: + creation Timestamp: null + labels: + io.kompose.service: meshery-consul + spec: + containers: + - image: layer5/meshery-consul:stable-latest + image Pull Policy: Always + name: meshery-consul + ports: + - container Port: 10002 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-consul + name: meshery-consul +spec: + ports: + - name: "10002" + port: 10002 + target Port: 10002 + selector: + io.kompose.service: meshery-consul +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery + name: meshery +spec: + replicas: 1 + selector: + match Labels: + io.kompose.service: meshery + strategy: {} + template: + metadata: + creation Timestamp: null + labels: + io.kompose.service: meshery + spec: + containers: + - env: + - name: EVENT + value: mesheryLocal + - name: PROVIDER_BASE_URLS + value: https://meshery.layer5.io + - name: ADAPTER_URLS + value: meshery-istio:10000 meshery-linkerd:10001 meshery-consul:10002 meshery-nsm:10004 meshery-app-mesh:10005 meshery-kuma:10007 meshery-nginx-sm:10010 + image: layer5/meshery:stable-latest + image Pull Policy: Always + name: meshery + ports: + - container Port: 8080 + resources: {} + restart Policy: Always + service Account Name: meshery-server +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-istio + name: meshery-istio +spec: + replicas: 1 + selector: + match Labels: + io.kompose.service: meshery-istio + strategy: {} + template: + metadata: + creation Timestamp: null + labels: + io.kompose.service: meshery-istio + spec: + containers: + - image: layer5/meshery-istio:stable-latest + image Pull Policy: Always + name: meshery-istio + ports: + - container Port: 10000 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-istio + name: meshery-istio +spec: + ports: + - name: "10000" + port: 10000 + target Port: 10000 + selector: + io.kompose.service: meshery-istio +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-kuma + name: meshery-kuma +spec: + replicas: 1 + selector: + match Labels: + io.kompose.service: meshery-kuma + strategy: {} + template: + metadata: + creation Timestamp: null + labels: + io.kompose.service: meshery-kuma + spec: + containers: + - image: layer5/meshery-kuma:stable-latest + image Pull Policy: Always + name: meshery-kuma + ports: + - container Port: 10007 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-kuma + name: meshery-kuma +spec: + ports: + - name: "10007" + port: 10007 + target Port: 10007 + selector: + io.kompose.service: meshery-kuma +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-linkerd + name: meshery-linkerd +spec: + replicas: 1 + selector: + match Labels: + io.kompose.service: meshery-linkerd + strategy: {} + template: + metadata: + creation Timestamp: null + labels: + io.kompose.service: meshery-linkerd + spec: + containers: + - image: layer5/meshery-linkerd:stable-latest + image Pull Policy: Always + name: meshery-linkerd + ports: + - container Port: 10001 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-linkerd + name: meshery-linkerd +spec: + ports: + - name: "10001" + port: 10001 + target Port: 10001 + selector: + io.kompose.service: meshery-linkerd +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-nginx-sm + name: meshery-nginx-sm +spec: + replicas: 1 + selector: + match Labels: + io.kompose.service: meshery-nginx-sm + strategy: {} + template: + metadata: + creation Timestamp: null + labels: + io.kompose.service: meshery-nginx-sm + spec: + containers: + - image: layer5/meshery-nginx-sm:stable-latest + image Pull Policy: Always + name: meshery-nginx-sm + ports: + - container Port: 10010 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-nginx-sm + name: meshery-nginx-sm +spec: + ports: + - name: "10010" + port: 10010 + target Port: 10010 + selector: + io.kompose.service: meshery-nginx-sm +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-nsm + name: meshery-nsm +spec: + replicas: 1 + selector: + match Labels: + io.kompose.service: meshery-nsm + strategy: {} + template: + metadata: + creation Timestamp: null + labels: + io.kompose.service: meshery-nsm + spec: + containers: + - image: layer5/meshery-nsm:stable-latest + image Pull Policy: Always + name: meshery-nsm + ports: + - container Port: 10004 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-nsm + name: meshery-nsm +spec: + ports: + - name: "10004" + port: 10004 + target Port: 10004 + selector: + io.kompose.service: meshery-nsm +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery + name: meshery +spec: + ports: + - name: http + port: 9081 + target Port: 8080 + selector: + io.kompose.service: meshery + type: LoadBalancer +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-traefik-mesh + name: meshery-traefik-mesh +spec: + replicas: 1 + selector: + match Labels: + io.kompose.service: meshery-traefik-mesh + strategy: {} + template: + metadata: + creation Timestamp: null + labels: + io.kompose.service: meshery-traefik-mesh + spec: + containers: + - image: layer5/meshery-traefik-mesh:stable-latest + image Pull Policy: Always + name: meshery-traefik-mesh + ports: + - container Port: 10006 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: kompose convert -f ../docker-compose.yaml + kompose.version: 1.32.0 () + labels: + io.kompose.service: meshery-traefik-mesh + name: meshery-traefik-mesh +spec: + ports: + - name: "10000" + port: 10006 + target Port: 10006 + selector: + io.kompose.service: meshery-traefik-mesh +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: {} + name: meshery-server +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: {} + labels: + app: meshery + name: meshery-server +rules: + - api Groups: + - '*' + resources: + - '*' + verbs: + - '*' + - non Resource URLs: + - /metrics + - /health + - /ping + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: {} + labels: + app: meshery + name: meshery-server +role Ref: + api Group: rbac.authorization.k8s.io + kind: ClusterRole + name: meshery-server +subjects: + - kind: ServiceAccount + name: meshery-server + namespace: meshery +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-jyy +spec: + containers: + - env: + - name: EVENT + value: mesheryLocal + - name: PROVIDER_BASE_URLS + value: https://meshery.layer5.io + - name: ADAPTER_URLS + value: meshery-istio:10000 meshery-linkerd:10001 meshery-consul:10002 meshery-nsm:10004 meshery-app-mesh:10005 meshery-kuma:10007 meshery-nginx-sm:10010 + image: layer5/meshery:stable-latest + image Pull Policy: Always + name: meshery + ports: + - container Port: 8080 + resources: {} + restart Policy: Always + service Account Name: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-snq +spec: + containers: + - image: layer5/meshery-app-mesh:stable-latest + image Pull Policy: Always + name: meshery-app-mesh + ports: + - container Port: 10005 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-kks +spec: + containers: + - image: layer5/meshery-cilium:stable-latest + image Pull Policy: Always + name: meshery-cilium + ports: + - container Port: 10012 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-smx +spec: + containers: + - image: layer5/meshery-consul:stable-latest + image Pull Policy: Always + name: meshery-consul + ports: + - container Port: 10002 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-mth +spec: + containers: + - image: layer5/meshery-istio:stable-latest + image Pull Policy: Always + name: meshery-istio + ports: + - container Port: 10000 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-qwd +spec: + containers: + - image: layer5/meshery-kuma:stable-latest + image Pull Policy: Always + name: meshery-kuma + ports: + - container Port: 10007 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-xcs +spec: + containers: + - image: layer5/meshery-linkerd:stable-latest + image Pull Policy: Always + name: meshery-linkerd + ports: + - container Port: 10001 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-lui +spec: + containers: + - image: layer5/meshery-nginx-sm:stable-latest + image Pull Policy: Always + name: meshery-nginx-sm + ports: + - container Port: 10010 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-izh +spec: + containers: + - image: layer5/meshery-nsm:stable-latest + image Pull Policy: Always + name: meshery-nsm + ports: + - container Port: 10004 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-xzf +spec: + containers: + - image: layer5/meshery-traefik-mesh:stable-latest + image Pull Policy: Always + name: meshery-traefik-mesh + ports: + - container Port: 10006 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-jyy +spec: + containers: + - env: + - name: EVENT + value: mesheryLocal + - name: PROVIDER_BASE_URLS + value: https://meshery.layer5.io + - name: ADAPTER_URLS + value: meshery-istio:10000 meshery-linkerd:10001 meshery-consul:10002 meshery-nsm:10004 meshery-app-mesh:10005 meshery-kuma:10007 meshery-nginx-sm:10010 + image: layer5/meshery:stable-latest + image Pull Policy: Always + name: meshery + ports: + - container Port: 8080 + resources: {} + restart Policy: Always + service Account Name: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-snq +spec: + containers: + - image: layer5/meshery-app-mesh:stable-latest + image Pull Policy: Always + name: meshery-app-mesh + ports: + - container Port: 10005 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-kks +spec: + containers: + - image: layer5/meshery-cilium:stable-latest + image Pull Policy: Always + name: meshery-cilium + ports: + - container Port: 10012 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-smx +spec: + containers: + - image: layer5/meshery-consul:stable-latest + image Pull Policy: Always + name: meshery-consul + ports: + - container Port: 10002 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-mth +spec: + containers: + - image: layer5/meshery-istio:stable-latest + image Pull Policy: Always + name: meshery-istio + ports: + - container Port: 10000 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-qwd +spec: + containers: + - image: layer5/meshery-kuma:stable-latest + image Pull Policy: Always + name: meshery-kuma + ports: + - container Port: 10007 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-xcs +spec: + containers: + - image: layer5/meshery-linkerd:stable-latest + image Pull Policy: Always + name: meshery-linkerd + ports: + - container Port: 10001 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-lui +spec: + containers: + - image: layer5/meshery-nginx-sm:stable-latest + image Pull Policy: Always + name: meshery-nginx-sm + ports: + - container Port: 10010 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-izh +spec: + containers: + - image: layer5/meshery-nsm:stable-latest + image Pull Policy: Always + name: meshery-nsm + ports: + - container Port: 10004 + resources: {} + restart Policy: Always + service Account: meshery-server +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-xzf +spec: + containers: + - image: layer5/meshery-traefik-mesh:stable-latest + image Pull Policy: Always + name: meshery-traefik-mesh + ports: + - container Port: 10006 + resources: {} + restart Policy: Always + service Account: meshery-server From ce20983430cd87dc6ae6f18a7a8a3727f6df1005 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Fri, 31 Jan 2025 00:35:13 +0530 Subject: [PATCH 09/33] add support for identifying kubernetes manifests Signed-off-by: aabidsofi19 --- files/identification.go | 78 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/files/identification.go b/files/identification.go index 706c1b84..39e8bc5d 100644 --- a/files/identification.go +++ b/files/identification.go @@ -4,9 +4,18 @@ import ( "bytes" "encoding/json" "fmt" + "io" + "strings" "github.com/meshery/schemas/models/v1beta1/pattern" "gopkg.in/yaml.v3" + k8sYaml "k8s.io/apimachinery/pkg/util/yaml" + + // appsv1 "k8s.io/api/apps/v1" + // corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/client-go/kubernetes/scheme" ) const MESHERY_DESIGN = "Design" @@ -18,24 +27,29 @@ const MESHERY_VIEW = "View" type IdentifiedFile struct { Type string - // pattern.PatternFile,etc + // pattern.PatternFile,helm_loader,etc ParsedFile interface{} } -// func IdentifyFileType(sanitizedFile:SanitizedFile) (string,error){ - -// } - func IdentifyFile(sanitizedFile SanitizedFile) (IdentifiedFile, error) { - if parsed, err := ParseFileAsMesheryDesign(sanitizedFile); err == nil { - fmt.Println("parsed") + var err error + var parsed interface{} + + if parsed, err = ParseFileAsMesheryDesign(sanitizedFile); err == nil { return IdentifiedFile{ Type: MESHERY_DESIGN, ParsedFile: parsed, }, nil } - return IdentifiedFile{}, fmt.Errorf("Unsupported FileType") + if parsed, err = ParseFileAsKubernetesManifest(sanitizedFile); err == nil { + return IdentifiedFile{ + Type: KUBERNETES_MANIFEST, + ParsedFile: parsed, + }, nil + } + + return IdentifiedFile{}, fmt.Errorf("Unsupported FileType %w", err) } func ParseFileAsMesheryDesign(file SanitizedFile) (pattern.PatternFile, error) { @@ -54,6 +68,7 @@ func ParseFileAsMesheryDesign(file SanitizedFile) (pattern.PatternFile, error) { case ".json": decoder := json.NewDecoder(bytes.NewReader(file.RawData)) + decoder.DisallowUnknownFields() err := decoder.Decode(&parsed) return parsed, err @@ -63,3 +78,50 @@ func ParseFileAsMesheryDesign(file SanitizedFile) (pattern.PatternFile, error) { } } + +func ParseFileAsKubernetesManifest(file SanitizedFile) ([]runtime.Object, error) { + // Normalize file extension to lowercase + fileExt := strings.ToLower(file.FileExt) + + // Check if the file extension is valid + if fileExt != ".yml" && fileExt != ".yaml" { + return nil, fmt.Errorf("invalid file extension: %s, only .yml and .yaml are supported", file.FileExt) + } + + // Initialize the scheme with the core Kubernetes types + // This should be done once, typically in an init() function or a global variable + // clientgoscheme.AddToScheme(scheme.Scheme) + + // Create a decoder + decode := serializer.NewCodecFactory(scheme.Scheme).UniversalDeserializer().Decode + + // Create a YAML decoder for the multi-document YAML file + yamlDecoder := k8sYaml.NewYAMLOrJSONDecoder(strings.NewReader(string(file.RawData)), 4096) + + var objects []runtime.Object + + // Decode each document in the YAML file + for { + var raw runtime.RawExtension + if err := yamlDecoder.Decode(&raw); err != nil { + if err == io.EOF { + break // End of file + } + return nil, fmt.Errorf("failed to decode YAML document: %v", err) + } + + if len(raw.Raw) == 0 { + continue // Skip empty documents + } + + // Decode the raw YAML into a runtime.Object + obj, _, err := decode(raw.Raw, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to decode YAML into Kubernetes object: %v", err) + } + + objects = append(objects, obj) + } + + return objects, nil +} From c9e24750cbc6a09a11c65d159f8f6a987ab8d679 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Fri, 31 Jan 2025 00:35:32 +0530 Subject: [PATCH 10/33] add tests Signed-off-by: aabidsofi19 --- files/tests/sanitization_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/files/tests/sanitization_test.go b/files/tests/sanitization_test.go index 25051a36..c0fc9ca1 100644 --- a/files/tests/sanitization_test.go +++ b/files/tests/sanitization_test.go @@ -72,6 +72,11 @@ func TestSanitizeFile(t *testing.T) { expectedExt: ".yml", expectedType: files.MESHERY_DESIGN, }, + { + name: "Can Identify Kubernetes Manifest", + filePath: "./samples/valid_manifest.yml", + expectedExt: ".yml", + expectedType: files.KUBERNETES_MANIFEST}, } for _, tc := range testCases { @@ -108,7 +113,7 @@ func TestSanitizeFile(t *testing.T) { identified_file, err := files.IdentifyFile(result) if (err != nil || identified_file.Type != tc.expectedType) && !tc.expectError { - t.Errorf("Failed To Identify File as %s , got %s", tc.expectedType, identified_file.Type) + t.Errorf("Failed To Identify File as %s , got %s, errors %s", tc.expectedType, identified_file.Type, err) } } From 1cf94bdd6037a93f2c786ff0bf6fc8dde705a0c2 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Fri, 31 Jan 2025 02:12:44 +0530 Subject: [PATCH 11/33] fix extraction Signed-off-by: aabidsofi19 --- files/sanitization.go | 218 ++++++++++++++++++++++++++++++------------ 1 file changed, 156 insertions(+), 62 deletions(-) diff --git a/files/sanitization.go b/files/sanitization.go index 5a8c4800..343234f6 100644 --- a/files/sanitization.go +++ b/files/sanitization.go @@ -5,9 +5,12 @@ import ( "bytes" "compress/gzip" "encoding/json" + // "path" + // "errors" "fmt" "io" + // "io/ioutil" // "mime/multipart" "os" @@ -68,7 +71,7 @@ func SanitizeFile(data []byte, fileName string, tempDir string) (SanitizedFile, RawData: data, }, nil - case ".tar", ".tar.gz", ".zip", ".gz": + case ".tar", ".tar.gz", ".zip", ".gz", ".tgz": return SanitizeBundle(data, fileName, ext, tempDir) @@ -78,92 +81,183 @@ func SanitizeFile(data []byte, fileName string, tempDir string) (SanitizedFile, } -// will skip nested dirs right now -func SanitizeBundle(data []byte, fileName string, ext string, tempDir string) (SanitizedFile, error) { - - var tarReader *tar.Reader - input := bytes.NewReader(data) +// ExtractTar extracts a .tar, .tar.gz, or .tgz file into a temporary directory and returns the directory. +func ExtractTar(reader io.Reader, archiveFile string, parentTempDir string) (*os.File, error) { + // Open the archive file - if strings.HasSuffix(fileName, ".gz") || strings.HasSuffix(fileName, ".tgz") { - gzReader, err := gzip.NewReader(input) + // Determine if the file is compressed (gzip) + if strings.HasSuffix(archiveFile, ".gz") || strings.HasSuffix(archiveFile, ".tgz") { + gzipReader, err := gzip.NewReader(reader) if err != nil { - return SanitizedFile{}, fmt.Errorf("failed to create gzip reader: %w", err) + return nil, fmt.Errorf("failed to create gzip reader: %v", err) } - defer gzReader.Close() - tarReader = tar.NewReader(gzReader) - } else { - tarReader = tar.NewReader(input) + defer gzipReader.Close() + reader = gzipReader } - // cleaning up is resposibility of the tempDir owner - extractDir, _ := os.MkdirTemp(tempDir, fileName) + // Create a temporary directory to extract the files + tempDir, err := os.MkdirTemp(parentTempDir, archiveFile) + if err != nil { + return nil, fmt.Errorf("failed to create temporary directory: %v", err) + } + + // Create a tar reader + tarReader := tar.NewReader(reader) - // Extract and validate files in the bundle + // Iterate through the tar archive for { header, err := tarReader.Next() + // fmt.Println("Header name %s", header.Name) if err == io.EOF { - break + break // End of archive } if err != nil { - return SanitizedFile{}, fmt.Errorf("failed to read tar entry: %w", err) + return nil, fmt.Errorf("failed to read tar archive: %v", err) } - switch header.Typeflag { - - case tar.TypeDir: // If it's a directory, create it - - target := filepath.Join(extractDir, header.Name) - if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil { - return SanitizedFile{}, err - } - - case tar.TypeReg: - ext := filepath.Ext(header.Name) - if ext != ".json" && ext != ".yaml" && ext != ".yml" { - continue - } - - // read the complete content of the file h.Name into the bs []byte - fileBytes, _ := io.ReadAll(tarReader) // Validate the extracted file - - if _, err := SanitizeFile(fileBytes, header.Name, tempDir); err != nil { - fmt.Printf("Skipping invalid file: %s\n", header.Name) - continue - } + // Construct the full path for the file/directory + targetPath := filepath.Join(tempDir, header.Name) - // Create a temporary file for the extracted content - tempFilePath := filepath.Join(extractDir, header.Name) - tempFile, err := os.Create(tempFilePath) + // Ensure the parent directory exists + parentDir := filepath.Dir(targetPath) + if err := os.MkdirAll(parentDir, os.ModePerm); err != nil { + return nil, fmt.Errorf("failed to create parent directory: %v", err) + } - // fail forward and continue - if err != nil { - fmt.Println("failed to create temp file: %w", err) - continue + // Handle directories + if header.FileInfo().IsDir() { + if err := os.MkdirAll(targetPath, os.ModePerm); err != nil { + return nil, fmt.Errorf("failed to create directory: %v", err) } - defer tempFile.Close() + continue + } - // Write the extracted content to the temp file - if _, err := io.Copy(tempFile, tarReader); err != nil { - fmt.Println("failed to write to temp file: %w", err) - } + // Handle regular files + file, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + return nil, fmt.Errorf("failed to create file: %v", err) } + defer file.Close() + // Copy file contents + if _, err := io.Copy(file, tarReader); err != nil { + return nil, fmt.Errorf("failed to copy file contents: %v", err) + } } - // Return a handle to the temp directory (or a specific file if needed) - extractedContent, err := os.Open(tempDir) - - if err == nil { - return SanitizedFile{ - FileExt: ext, - RawData: data, - ExtractedContent: extractedContent, - }, nil + // Return the temporary directory as an *os.File + extractedDir, err := os.Open(tempDir) + if err != nil { + return nil, fmt.Errorf("failed to open extracted directory: %v", err) } - return SanitizedFile{}, fmt.Errorf("Failed to open extracted dir %w", err) + return extractedDir, nil +} + +func SanitizeBundle(data []byte, fileName string, ext string, tempDir string) (SanitizedFile, error) { + // fmt.Println("temp", tempDir) + extracted, err := ExtractTar(bytes.NewReader(data), fileName, tempDir) + + return SanitizedFile{ + FileExt: ext, + ExtractedContent: extracted, + RawData: data, + }, err } +// // will skip nested dirs right now +// func SanitizeBundle_(data []byte, fileName string, ext string, tempDir string) (SanitizedFile, error) { + +// var tarReader *tar.Reader +// input := bytes.NewReader(data) + +// if strings.HasSuffix(fileName, ".gz") || strings.HasSuffix(fileName, ".tgz") { +// gzReader, err := gzip.NewReader(input) +// if err != nil { +// return SanitizedFile{}, fmt.Errorf("failed to create gzip reader: %w", err) +// } +// defer gzReader.Close() +// tarReader = tar.NewReader(gzReader) +// } else { +// tarReader = tar.NewReader(input) +// } + +// // cleaning up is resposibility of the tempDir owner +// extractDir, _ := os.MkdirTemp(tempDir, fileName) + +// // Extract and validate files in the bundle +// for { +// header, err := tarReader.Next() +// if err == io.EOF { +// break +// } +// if err != nil { +// return SanitizedFile{}, fmt.Errorf("failed to read tar entry: %w", err) +// } + +// target := filepath.Join(extractDir, header.Name) +// // Handle directories +// if header.FileInfo().IsDir() { +// if err := os.MkdirAll(target, os.ModePerm); err != nil { +// return SanitizedFile{}, fmt.Errorf("failed to create directory: %v", err) +// } +// continue +// } + +// switch header.Typeflag { + +// case tar.TypeDir: // If it's a directory, create it + +// if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil { +// return SanitizedFile{}, err +// } + +// case tar.TypeReg: +// ext := filepath.Ext(header.Name) +// if ext != ".json" && ext != ".yaml" && ext != ".yml" { +// continue +// } + +// // read the complete content of the file h.Name into the bs []byte +// // fileBytes, _ := io.ReadAll(tarReader) // Validate the extracted file + +// // if _, err := SanitizeFile(fileBytes, header.Name, tempDir); err != nil { +// // fmt.Printf("Skipping invalid file: %s\n", header.Name) +// // continue +// // } + +// // Create a temporary file for the extracted content +// tempFile, err := os.Create(target) + +// // fail forward and continue +// if err != nil { +// fmt.Println("failed to create temp file: %w", err) +// continue +// } +// defer tempFile.Close() + +// // Write the extracted content to the temp file +// if _, err := io.Copy(tempFile, tarReader); err != nil { +// fmt.Println("failed to write to temp file: %w", err) +// } +// } + +// } + +// // Return a handle to the temp directory (or a specific file if needed) +// extractedContent, err := os.Open(tempDir) + +// if err == nil { +// return SanitizedFile{ +// FileExt: ext, +// RawData: data, +// ExtractedContent: extractedContent, +// }, nil +// } + +// return SanitizedFile{}, fmt.Errorf("Failed to open extracted dir %w", err) +// } + func IsValidYaml(data []byte) error { var unMarshalled interface{} return yaml.Unmarshal(data, &unMarshalled) From 4824d90ad605d5f5c83b71ed1d8cb8c8f66664fb Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Fri, 31 Jan 2025 02:13:36 +0530 Subject: [PATCH 12/33] add helm identification Signed-off-by: aabidsofi19 --- files/identification.go | 56 +++++++++++++++++++++++++++++++- files/tests/sanitization_test.go | 16 +++++++-- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/files/identification.go b/files/identification.go index 39e8bc5d..660804e9 100644 --- a/files/identification.go +++ b/files/identification.go @@ -5,10 +5,14 @@ import ( "encoding/json" "fmt" "io" + "path/filepath" "strings" + // "github.com/fluxcd/pkg/oci/client" "github.com/meshery/schemas/models/v1beta1/pattern" "gopkg.in/yaml.v3" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" k8sYaml "k8s.io/apimachinery/pkg/util/yaml" // appsv1 "k8s.io/api/apps/v1" @@ -27,7 +31,7 @@ const MESHERY_VIEW = "View" type IdentifiedFile struct { Type string - // pattern.PatternFile,helm_loader,etc + // pattern.PatternFile,[]runtime.Object ,*chart.Chart,etc ParsedFile interface{} } @@ -49,6 +53,13 @@ func IdentifyFile(sanitizedFile SanitizedFile) (IdentifiedFile, error) { }, nil } + if parsed, err = ParseFileAsHelmChart(sanitizedFile); err == nil { + return IdentifiedFile{ + Type: HELM_CHART, + ParsedFile: parsed, + }, nil + } + return IdentifiedFile{}, fmt.Errorf("Unsupported FileType %w", err) } @@ -125,3 +136,46 @@ func ParseFileAsKubernetesManifest(file SanitizedFile) ([]runtime.Object, error) return objects, nil } + +// findChartDir uses filepath.Glob to locate Chart.yaml in nested directories +func FindChartDir(root string) (string, error) { + matches, err := filepath.Glob(filepath.Join(root, "**/Chart.yaml")) + if err != nil { + return "", err + } + if len(matches) == 0 { + return "", fmt.Errorf("no valid Helm chart found in %s", root) + } + + // Extract directory path where Chart.yaml is found + return filepath.Dir(matches[0]), nil +} + +var ValidHelmChartFileExtensions = map[string]bool{ + ".tar": true, + ".tgz": true, + ".gz": true, + ".tar.gz": true, +} + +// ParseFileAsHelmChart loads a Helm chart from the extracted directory. +func ParseFileAsHelmChart(file SanitizedFile) (*chart.Chart, error) { + + if !ValidHelmChartFileExtensions[file.FileExt] { + return nil, fmt.Errorf("Invalid file extension %s", file.FileExt) + } + + // Use Helm's loader.LoadDir to load the chart + // This function automatically handles nested directories and locates Chart.yaml + chart, err := loader.LoadArchive(bytes.NewReader(file.RawData)) + if err != nil { + return nil, fmt.Errorf("failed to load Helm chart %v", err) + } + + // Validate the chart (optional but recommended) + if err := chart.Validate(); err != nil { + return nil, fmt.Errorf("invalid Helm chart: %v", err) + } + + return chart, nil +} diff --git a/files/tests/sanitization_test.go b/files/tests/sanitization_test.go index c0fc9ca1..0e276c44 100644 --- a/files/tests/sanitization_test.go +++ b/files/tests/sanitization_test.go @@ -76,9 +76,21 @@ func TestSanitizeFile(t *testing.T) { name: "Can Identify Kubernetes Manifest", filePath: "./samples/valid_manifest.yml", expectedExt: ".yml", - expectedType: files.KUBERNETES_MANIFEST}, + expectedType: files.KUBERNETES_MANIFEST, + }, + + { + name: "Can Identify HelmChart", + filePath: "./samples/valid-helm.tgz", + expectedExt: ".tgz", + expectedType: files.HELM_CHART, + }, } + tempDir, _ := os.MkdirTemp(" ", "temp-tests") + defer os.RemoveAll(tempDir) + // tempDir := "./temp" + for _, tc := range testCases { filename := filepath.Base(tc.filePath) @@ -90,7 +102,7 @@ func TestSanitizeFile(t *testing.T) { } t.Run(tc.name, func(t *testing.T) { - result, err := files.SanitizeFile(data, filename, "./temp") + result, err := files.SanitizeFile(data, filename, tempDir) if tc.expectError { if err == nil { From 16a5fe7c9d0a9715072f9fa2e2beb09db1f33274 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Fri, 31 Jan 2025 02:13:45 +0530 Subject: [PATCH 13/33] update samples Signed-off-by: aabidsofi19 --- files/tests/samples/nginx-ingress-helm.tgz | Bin 0 -> 25889 bytes files/tests/samples/valid-helm.tgz | Bin 0 -> 27436 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 files/tests/samples/nginx-ingress-helm.tgz create mode 100644 files/tests/samples/valid-helm.tgz diff --git a/files/tests/samples/nginx-ingress-helm.tgz b/files/tests/samples/nginx-ingress-helm.tgz new file mode 100644 index 0000000000000000000000000000000000000000..786f1bb32acbc842ec1595f488460eef2e18de5f GIT binary patch literal 25889 zcmV)=K!m>^iwG0|00000|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PMZ%a@)AlD7t?8DNvbj*Cg|WvMzQmu5Yb6wl218>tc$OY|UOL zfk;TAMG_nWv@Nfhs{I=KDb73XCpiUxSBW>-PO|cC^G70+K%>!UG`bs&hP^QC2g$G( zE+S;Vxr8pQtf6K8?FFA|wOajfaKQhsR;!!;S3ev!{#I*zJZKz#s2|oEf2-CGs@2-x zK=mb&$a*|Nq5HS$v)kJ2+)wgA#~34*7}!1nceOp}IN_hQ%I8XTPe(*^4Tthq{nKv$ z8gvD>fF`!7i%k=`dxpk7FDZ4%(f+;*?<)&~F1?vYkgH*vBAZq;Z0&P)Gi+^t<;{?5 zBZ^4Ke0RLTeH}R_UR%hfWPgcFOVSuq?(=^o)=tH-7kewTzQ?Xkj`k$r1K5)Q2fxQ) zhrRgqAAbk6O6{Ohm8!q)S5(HahbBQB4p0mWVUGgC29{x(209^@36GI)qIYsQdwuJTqv7SN7VH&nU4QK+!y@K+fh33#R zDKH2)s8*}stscFoGoIumLy!*&vf<1`5_BBNaWO?2FGgJtz2m^T^I)*5??C`ahzEiE z3;~ZIJ*11w5%!631gbufN1o$gS9J5(allYCwg&)o9KU78dAx9&BgOj{D+?^FdnW60 z1~n_ZzUWbWXaazlL+oIkur7K;G`hru{z`z2=sk8T1dK)^Ld5eW% zFCxyV0G0Ty3nu4fz|63ryRHBk{oxrdA^^EAb|n)pBoo~s^GLu7I^4we+*tIW1MUn6 zzDD?0H2$T6Z0Z__d=1B<#i}QXiM;38Zd0b6+hS=$soa56F&e7P1) zGV}xrL-ywp`2C*@MZ@Co5Dq&SYj;; zpcF2Sz#G2AzezxFJktaYHVtjfdsz&M)fn;$N1!|JV>)yZ@kN>7f<*V@0z6_LHebPP zolA&a1o);17lEHpd+BhDjk7;{gnXd^ZIt@IJa=tFdo??6uc-P zdoG6-$O#T1fc(oZpvz^Si!^Lm$kve_$b>caT!8Owpj?iChFogQ4Gl6rg|-e>XifG& zARLv;QDO*Ykf09-60$YSAZ|bihdmIjXNoc$0fO8+L}YUd-hUx%xH>uS=OURC?D-0lqJTeGU1^_P>*vVElTP|WGwq%EtXYo{-t z(gS5IclyUp_)V{xsyvzYuWnDyu9VJLRyz&7rmL_nUyM&5@W?u+(=!uJ+J_%hrF*DP z8|tXuuT)wt8STAj;Rs%%fI+12d6swvaQsrBh@qONK;_3?7Hx<8nl_8Mx`u%@RkjY*X>~k=BWm{Q4plmx2iZIs-PBf( zW4k|ukDU%urze-yb9-W*k7uj?*;J*cs4b8BReGhLF4s+q{xP~9duT;f`C47WmU^I= zPIXkTjWqTAOmDUCHKi}#JT9k7qy5mUckG#k@6?HWt5v5rH_Ex)yFIC0)XbY{OUKj3 z%AKyr#bkAGH&#~snsXV@R`d8IXLsmN{<927z zT%Fsn(ZR58o(-Rz))~eH!EA?}W^TouuoYkqUpUOk6Ot`@DJiXezTRxz`NZnby?0YK~obLMNI*6{U0B=qtK9S>VU%qELFit>xNutxO(n zN7d=gmHMfAquex_E7R6$%g37)?a!2}`*BTeXs4eo?bM`dZEC^m&PZR44{xk~qhIac z3|gHJTBq%*Yg$8R^36qkd@^l}tNpWOV{CR0^?G+bzP`CY=F*$4KA+7d%Sm5Zw)C3b zy|Viku)2EaUoRo7kJZVwKI&I>1GYQ8(Q2xyt5ri^E$^&b23 z?KkA>{*`mCwmOz#I^97>y&Fs(4E3y!m3plsccv@-wsSaL)3H20f0&Kv>Bt~;xr!f9 z=loPVo!UdCdg05+j@IH;EVLaC4xbE z{&KKFYf*cJ)?2H|Bh~C^?aukts&g``_D@uGe4-nl zYx1ObFsq%fdS~k0^m=@*ob@{wHTh<$Iby3som6}6 z&&%;C?V{E4Zr13GCzf_UJ$+3eqr#<)7GEj!9;`8ZpXwcHph%4Aiau4?znxYxK*m!AIEp}k}0VY1Ti zNu-uXG(PI{-S4ns`qYBNY6AMw6jiY z+NMoeSvKMNa}}AL?pQG|H1+UXH` z`EqK?4@zxW8@2mu)pQOOTfLjsyLYO3bud#qgQoQ`=-tXwWnxy9iF`h2Ee@6T6e}yI zaity(2CefJQnW{H)jOO$j_=f#xq^?!cQ=o6cXoUHI9iQw`j6AYtJ}rnmG#+`Yn{8B z6}jl$E_?k|)z#0`N7d^5(ZAI{j4iFM*U2MnFI~0jT#p`4?#HzocXX?}>Pnv6Sj*Op zI$icxhjve$_T|aLQZe!4xV2oWDqUYKoR-?^w@|yI=(jfy6J>R%w7UlvkNxY(xYNC8 z^lzXtKJ8U&&ds=gGOeAguGCdy(q7i|>rPv#nRh+)aClig$D>nPgX2a0%9d%<#%^Dk zdV1ZN_ST=)>Y9#ZtAC<3&U=d8z3;Wor&HrIzNvOyI5u1T$MNLG($@M||1eQ*8p=c; zD5qD4Gj6W)zz)O zRMaWCsjlksnOq;$+pcmtRg~+_`ljv-rnl$jsO?Pq_R^bK4;T8vIYqW}dRdo^@no#b zCJ!UDlB<)IZfe)lYk1b_j@F+I<+i&y44+mF|H*RzFxiPKYucv3r3uNmPWUAG1^V0#m)z6?QtNrTX$#m>=M)l5}eAZdZ zr-vsulhqnd4)Ij2osTEeGx@r6dhvL^zM6bKyIOSyz47tIMU_r3t0oyW(mtxq@TIA6xoiVvySG^z&e#oX{Jyb1)t+4{y}_i{5Q#q*|Y8 z@AUjuvvs0a<0hQ+J0r_vbKwToADq#6{4i}SN7I$r@70$}bu6pe%DkAi`yF`JA>(Qd zj;jyDS-o5DEB*C!fg7-6_D0ve+Ek@0*uYiTk;l62n7Z{@>79+QHS2S8Tz?ph&&*S3 zciqusc`>!8s#aY(gHxTTjm~Ozy;zS{=gX6!kaZyvjo*0eQfEpMmR!_ByZ4|@~m=CUF8 znzx;XV#ybyI&DpBW^=ZtRrT@YPXPA-QK;k)U5}s zsN<%5dh;;4HR-5za;UXV?k8#=j_UG2KkMI3+f#bAqL1S?z3SbbFBOZD@p#!BH5TjX zqs+9$&%-M$bPjbqkehir$%ghtkme!^?hU#GaLYpXP5gXiEDnx)M}g%L3g+C^#jA;* zzFW|O^ayc6}$-d)^T7v}K|l;`@t)YuiO| z1)#kKkWyqh+~tJ`g)`G2A**29Mq1=n5?+!wZ_4WTKXYHXDX`Ji@{{SMdj9V@=VI)i zG3nwZUToF{j$f>^p)ip~vDm_LFzXvJ7B=4-%W*e5SQpz|SVtotM9fRdUEj#H3|nlu zEnKJ};2_t)dE0aj<|7uXdCDpddA*U?D?0*>YOEC(`P=IGU^%j3S&pgjNt=dcP~r<8 zHZ>jZ2-K=oD|}-ii@W+i9P|wTrHlUX5ZR$xj4IefZn~2@1>;!hVE-a;me1#gZP1*} ziV#RpbYpz(u<2Q)=;oK?7_iPsz9%0&P#1Df{+>^Tz{7szzX01MHxcMz&!*+E5XFm8 zu~@kwq{}0)kEkZLX!cowioVZFS7N1!Hxp%JUt$x*vu4O;D7wfo4Grd@o805}D>NB4 z@jWulgd_(_#V^>wZhXf}gr|IZ&jbv1T|^vg>rANl5Skonz~(}}(qM}R{oz5A2`av) z#!GTpI72?pNUwkaT_X41An3h7Aa6^04h;@3+Ey0iN+Ali%{6dbtRX@WiA`EA%xB2N z_JV;jDDhE~fKfpeu!MJFyY-C`>tY{T=na?~$ke0WZuc*PivQ&kF{5ug<0=~A2uJuv z`%>Ud6wy;L@V?)X3a|Sn6>Y&Jx^9R~Pjj7#Y$l#M<|})MkDw}F=tZ!B25mm{2Ju(W zHTy~#H*tDP9znsDro?MYj52S)+=Pq0?a@v}3YsQ|Ha1-9L36|v;r+y4GWIAc!aj5K zc|?S10zdqTPpKHW_W+33^fw=4Y!Q`>K=`u^(4|3mZ^Iyl?rDD@w_E-Ibr&{vBLYNS zVDk=pBK9JG@FOnS1HdCFK92{sxlZoOvp2lQ+af^}=%f1pofsO>2AkybsTb-=VGqUX z&z-y>&xC~xJE5DzfpP_jAbL#v$PyooWC!|zg2MRy9#U<|yR${X3OHGZHdKb3<<)%_ z3l$$e;`~e4RG@q&Lq`1Ags8>6ASk5CzsQlQKwU2xzQGmGAtZz@UFo=GElfOvW;4Do9hyl^7xCR?)|=Dqa+^Ke5itNX4nKxWq`f;x z3zn|5mghXui|HJ`ED_yE?ytub+589-z}pKoHlec-ibPmu@G8&)bel+??u73FADJU? z*f?y&U%HrL4Vy z>^jzU0D*}$(GasI^&r|t*gkj;mI#`3DG>^(M{TI%?RB6U`yx^$XzrQ9l6&CRk{Mv2Es?fj9FiMtBMOgrL&@Hb7!6rPRqjR| z3vdcC_v4~AdzY9DqP2Kan@y{7_Xxc4<9x&iE>!pnAG?&mG%@~!db2MAulK+=siI&( za6IfLgVF+l6LeG9>`cJiFOi!8IUO)F@I}~_Zwd=W{#5-`4O^Hj4T^ZoR(E)J0J_8d zX167SnTfR(kK2OWR)#$2ko{e40;ua8N?vqkx1@ z3ZV|r4FjlCL#X{y$C}37S_MLc$v{@F$i*jI>kJF|NWYT_!`QG=04-mV7LA;?Xs*cXI z4DK-smQ!w;`+94jV9jH8ebUju%=>U{3t$vPu>bN4xWS%SIiN|xy$jK#&@+4xTcD9a zm&gsdFy4AF;CBX$H<{A^N>FFySMeus@H((h!bWX28mVU-GLJZH#Ia?U%W9Ou6(UR* z(-;%5NjwKL%qZ@1>5-?k`#uvAxnDOXMQBKISrlL7L(*;^3W~@3hl+hjG;>`^Q$07C zA|{e}kpKM>**=d32KyG*#a%N+BP-0Gz_sj8QAV8vcCqtDFfqfK6>Ryw9WJ`K90wtZYn zb6z-Y-gD*)1Pm(?`p*C)8@+okm;?qothVAJ+DsCxWY3bA=kmx-V&6=_y~Nt8&js4&Gd;B(^TIWfda z1od^QyMpj!QT~VO zhw8@p@56(Sjo0(v-{Vn2A76?PW3zpRkR6=0MtbE(|AVC$r zkzEmKi1_b6*^*~#;6sD|G^~*~pBoS0jT8Yxw$6VGEX8gDFyRcDqzEffN`QinQ%UaZ z3)BB!{{n`snVz1}rwaJ%ue~KQtqNI+G&Q_Vycd7{B}H%i)HEQqNekL=fplrM7C-{R z1V(jgB`%(A4JE7iEvJwuYak?z8N&x8iQRHZ_%pL)sE=(Bsazi2 zSZwn%@eLqJ)FkDwg^p7WN$5B)H}q+&`J)&09iA-xFY2KbEelV~3sz+ReXMTe|Et%k zhaX<`zwhyE4wo`opXA%#a_I=pOcD6SN6od`ru$ZgF;_$GNNp@a$V6IoU1~ggk^PPmg@>o|U2*>y6o9NtFE!Kn{y+B zQFGUmpkGm;?9Sz`AmwICY)?Ii-$+3w+_IJ!@KOk6w884f8VCUoT|7fa@u8yBIYl)7 z3IIo(h>h**CqHu}GLdzXYfEi=HuH1l9-sBtrsyHjJdcYWy2hPhq6KOb4T_A+{#JEV z(Dm$Nq7vjDfm*c&ei5hkVnl-uoEfG;4MdXt(_QQ&-$>y2@-p!T>iTApep~r+((Sjt zjN0_$z|lS4)TkGMXpf`tQZb=Rm=%gj#cEqc7iHjGe~FLSzEpB-_2r2EIdgH@)MPjQPe}OdNd9=tBJbYm9sZ zS$D{7V#|Rp0&o7SR;hieRNsI~GbXhv7ty^Fl~aSp_W2Tcq#wf0Kq%z^ap`3&d1!VE z5B^ySMMDMx+d#7Dn>dD;0WE$2FCT#9xkd>fs@%<(Ks<+f-h}2^(zhz#WmE~WqGz<0 z+w0ho!q}wGvBy%&XST|h^b$=6I#4zszM4y#p}S?e5*_i|L9@z?kljQ~6b)yJz;IX4 z8zJdiQacP27X>mpT@rM-duz;j$a9*rWh1m1yiNDKB~G=xiHDNEc~`0z`HqJY($Zhh zHujTuAy8U)@MbF;;3X4lD=w09`jEDhzuA=>p%}})l(FumY&~WP$5ddOd{9tPLoO{U zvqI}qXzs9fZf(_;VTZ_~lKkk>zo*+<+cq2rvt3^jC;zu(Sc>hIhLEr{rBHXw7^u{w z2>FbXStSh(HUv(T?;#h9WNL^kh(S()Tu?#Mfc&1Mq@2haA5m0Fq&L`wsqDuFUo%Pv<<+b{lA0i;pYBd z{o}{i{lD+=l-d6a$G>O;FuGyuzs(L{a7HZKjTa?@6D_~)8NTir7VR0nZpFQB#eJWx zxEI}*e#w2kZ@WqN)Lp|Ly|1^!2HxvN-Rnl(rd9P~TP?3Uey=-zPv7x--KcxrsC(V0 zd)=se-KcxrsC(V0%kRbOM&0X1-Rnl(>qgz{M&0X1-4A$5?EeW?OWvfv(?(#?`QO9C zjr>pbM(yA=|I>GQGQEiTvM9a@xcWqdwpqdMV*joNO=k&%J;9XABz8JfY=8gb#K9I# zj_)UpIQ$BYZ*xfZ8$KoGf5HT=JRt_4aQ=VzxViuT@%8-w_j%I&Cja>Ru8xzqK(NLB zk6|w=Og0#J|E@MeG?WQ(KS{bUDDUt|HOaQFECSo7{+V>}VG73HNU20Z$CoCqneum{ zV{sN9g}HPIYXh z?h~a-3~_bBPU0eUOXz!v3US5Lz=3~wXfME+nxvv-2Lf!|rtU}QnWVz6!!E4jdj|i_ zq(Q0qFI?PD761kF|Dg7vwmJU~4`1`YewQatjgD`C*ajT z+PHEqze{Nl63+A8kceO6wvA5Winnqsm@1v>*0ZLYV9jo0wK- z)4x`{jTk@)ZmLUcPDp;xU4B2qs`1T;_+4Kmt4CD=Aj#`yK^VB|$BXVT55id207srdXYx!!lP6G`d`D7~(zM<@Ny1Nd>~L+Q--Yzu)B%{y!cBnc)Q7 z4xM`k-o9M4+`E_bDW-?Y{X07qw0BNVwZHc?`x^1{%gOY9ntA_ykj?|*ub&vR#?>zOFm7m<3fM*`3> zPF?Id8Ho6Qns}jMd(Qs6L+&gnw?I_<;D0H;{e`0ni=V8X+Gc-bj)kwm@!$(rmY21eb_EZPd!Qz~f-jntAFel4tdH z-#=%Pz{Vf-QXZ?!u<g^gYGf^8Jp|J4s0_J8f5{(Aq<_j!H+Lr5udZ2~9; zV(7(%?=#OZncxNv)K+kTNM-N0ieoQcCB=WICprJ+_HnD%u2}kuwvj*o4-RXG^^N)8 zXf$5q|GvxfOZM&WXUMbwzp{95@0VXdmwnJZZe{K5DNBQho1cNm4@meG!bxC}whas$ ztg+{Ui!jO_XhOHCaOgu9x#$niAO_Qwxqf*Jevg$7&Kit7hv~^k2eUPAavSr1zU4cu zNBjFa)&eD1!`6O+*<*qE`xI$Q+t8pX5eI3;+|Yz>zW)menvVH=F`%{C8AKf|Y`~Uf0|62#9u|iDHdL*FL(DZzjRRSkWMJ4g|5(ZV3qT{A+>v1#IaCe0bi44Y6_j3fbb7hB-> zC34X}-&QL7mC8Ps{E;kpSMkwPxx&vOe9g(#^;IN@32v4C%CpN5p8JJ+6uWaQV(+p}0mh=ew+n@yaHctf{ z>$<@@3{7+Wo)-;(LX+SK@*KN>U&0+i(@hR}^oe|R zXF&cJ!vsVeU)kG>b>}~#&j9K=2orbA%!uCnzW7KoiLZ&OzIh$=rMtlpfn47HBDcui z-fzG4F-5=qcH~PR)+I43VluT>sU5rr7F;1fJeLb5hVWNOx`U>{n*aM8ngo4iU-(>R zBcxH&cVT@68n!GZgJ#IY_Z2XpOXS`g1ifc%f_uX>x$hI&0J$!9fe)9n3ODgt_s0H9 zY`0mVw@IH&E1Ylqh~>K@;Mpr1-`l>JdE>Z<`zloNy2IX*H*Cc*FgJ9eW1t6$Y%a4U z=orGaQE^ra_Kg<+QHhFwTCI|W$H+mN7?+c9M!zqd{SermHA60wi^Vb};6KKbw%|a2 zxfHD2s5}78G>}aNmHpy?E-?{2P}v%J7)lSnsu(33A)PD#AqZdIFm4Y<{Oi#Mk0&_+ zNOPdMI@uR=5iJ89^OFT^u}hJpOk;pFAW#f*@K(rE4ZAKP4!^&U;>4Ph>b(032;$4& zSD$McXMWA|{BPEHCn%yp8<3UZgo`CKMUJ~*O%wF(#N_dNU}FG<3)&!@rbvGeAYm)B zjuGLM*d%cbMuWi++Q*vBK-Rv*Ncxq_{ZQ%zApDtE*^&Wl)V8Hva(IO_Tw$+eaY*`MfHtkJmW zOlohDoKL_P>J)ej3Gpn355sZ^?-19L7@w!Fl|sAv8fB>q+WDHTR(-YB!S4!sfRg^jeY$Hi^=ovipkO9^uP zC)36FZNy#y;_ug*2BD%o2fENg6uG`)z*gZ|R1N7s_grq*#a85s=)+`*Y#;eR58g(9 zL)B$!3GWTlC6&t&(2z@o?T@(XutMu_?G5@RR7c=Krs*;~lihY)BrTAQTu6})u+1z_ z89^bYeorI}`RChT5`Vu7Ny3$Bh9N-+EXB`|3IOhiYXvY%K#(-8Cy097`@2M}An0Jv z)`1f`8`%4U>H!ctfI!1K;ter&W2zfs!^d9e*n);Feuwrj!`DSjHwuP>hi3?d7cuSS zAKx&)*yVf~)(T6#2d*dBEpq1s4Kse;XeCQy=O`x?b~XqBnUn_J_;9EE_w3xiu6S@gdgir*@ZqGqK~H} zIi9;fsbJ%at&1#thX@GNt@x^nEeG4krUXDjux9X?vZaZMCEMgxV(%*%)D;qO>vaGv zlve_=n3byF&39ZDWl5NT4%s5cAR!gF`@*jgDxRU`&t=@E-yl#!0^I{@E+&GGVUz`_ zfT8FXvV~NLuSV&v2=23jHungu5OO$u6ZJ5cQ>ktqf5~z^TL{dg*M?68+5gt_Olml0 zn!kvI&J)&R0L;p+5VgEX70xZf&!KO1_;{QM})h) zVhd)VU8Eow?3ReTka($d&~C`CtEgi*bQxQ^S0*5gZ}5 zoNNw>v888{-8Z1(D($}CgcLN9Qi1Hh6FU`5SA7 z@mKIC*G0HRA9W=VC4*Us@J&HqRJOO`Y*m?k;UvicWKjHq9VlD9Y zg2q{M(Whiz0(<-`?|iO(HBVT43@BGSx8Wu3I0$;;-;KZ+*D_4gAmXl&{M{)&^Y5WS zg(3rpr)h|gxo7&Ah>I5hoO4ObM&4c^)_H%%bH_C_nStO^Vd(|jU=VOntyaNX?1Flg z{W9i74q1cndqm!G_qFext-eoKE}Y2(;=W$VC{;~RDRvJLBTw5FoS#8=2MZT-;HhS{ zeKcfAO~*LMksol5i5GykvpEqWMqz+zd-?mm0+?{U z#qMs(oLoEtlhI*Qb=+!&!bQmCaFI7%d2xm--woV`6kXgD0Az1OtZigM=Rxky`t36^$7;g*L($l2pkSU4A3693w$n@w6k^1d6ivfj_~&{z`)#@Qx4H|8u(= zj95aiFdiA73wg2O`PptHO6R~@IOeNSY*UO!L9@w-9vn;%GZ~|$XnV0`bGHIIe2EL! zv^!)AO(L#SbKe(P+Y~+s)PvifM3W5F-~X9FbtB_bK#&|Qfn8reE`hx;l~ zu$2^gxMH9SstOjHn!o$nPb<2^ay+5nn7;Q}@8 zxSzxCu~3iGQ1d-#c}%^zOS(D8ky^-T*~fNrE!jN5xWg<#cgS6bLW(WAVl0=C@bZE2 zGV>@?-TlD`8HG=#gy)!Vy)s@x^gVBZc-k`20PzKh&>ma_SKN66DMgmUl?}wb83qXl z9NV^V%ocXP+?@FD*6XLvm}k@B-)v%SPlU~xwZ$t1Q!9R#Yp79R9fg@;I2|3#`oKgt zH%oF<7^dc?tjH9(mSOYt`qYIQ8pdxL4Ariv5LmEM{)L1&ss>+~{{)Ss8Wy`DHiR9! zG&UDp+9Dm?{2Ho3!A-EmkT&SxU+5YD3hsB^8Kd~kfJtEfZGk&ObfXX#0WNY}?cB@w zjnyGjg^@|Yeb;#wcf!ctunA&Wx`lSfPYvdo03DpzPyEN&-e^pwuuVLWsn~d86 z-+;LZ7onk>m@Fw*2C_NR6^C%PXvo)8>S*W6L~8XGUcDpBdkAL~ywK$iyLXofzO z?rDD@w_EAXgOvRG4DCL;4{(m*3T^Opd&Gkf@o+H@TNVqMAJ^j#LRiSKL8h>&0Dk4- zJio8$JxX4k$U76B%A{Bvv<0brT>()DXqLp{)zS9$#Gl~Jo4pc2je$!kHHMOb;c-6O zfRfi&#o#dIDt@sQwz&li4h|Y|7gQl;>BP*wIEJCn#A6$He6By$1;CAPmTlj8x(jV%F;3nN78@g<4k<1b5L`IMrb9wZa(y#TaHbO| zeoH4`vvnOnU}DV|_r7yZ6nhg$-OV5q;YRf9gkihkj%B@C-3}qK*FgvgSHwq3=E zLObkB+n@jBhrS|S(HpV zXt6s4uE;L^mQBNVLZjxf1Z<9r(?^#;*nJTff~(4 zI+It>Om}K#B?n{e#SWW?i!k>Y__}$ZBhF(^hoq~7pE?Z|!2m{%32}oxv3&3Y;lW-a zZ%gPYe%&pYe)Mu6(c{eqW40NkQR1TduYvyiHM%z5zx-dc`<3s^ub&VS679Wa7pr8p zO5q9-rqF3@bTxi^`V2FAyNoMnsu(HN;XlF1_w~e15p8gPXi;+QH?9zb1F#+M6c6JF z4VrM2%ylK9*={mR(6qk=EkYAThXS9#7B|L??y^kJnQw+dS;dI>bt|F@#%D1%`Gbuz zj#r*ZVvr>4tqrQA%#0P^P`vVFN!d(|F~e7U0KpBDTwT1wh8|gXo7)nx0sVdhQIf5b z_aI(kZ57y=+1wpY-uIlr0~yCkgf2q>$wuGb3x0%wTCJ_Pyf>5Ntbiws&L$05&vzg9 za+cc$zPTYv4%TH9P zYT~DL1z!V460iNnmjvPG;NVsmvMC&ETnGr4$r2K7YcUoH1MB%&8WGIt&li_^PDRAz z^OY|=Y9>Ox1H2p;&$c}aN$UEWDIUq}RmLnzUiiOlA_UE*1N>C|RNMO+%U#U9FHtuF z1xPgPxY$B;i98Z3-4<4vAY38}7g&h8hDL%hABNTa;DXqY$3O@Dh@la>;V1?~bD<;D z8y`Lc^SM@Q?C~&ewm_4;-y%N>kqk}TxC`cn87)ThFzh+|;3L2`=$`aIliSKeaqhVS zMo1v=`^>}(;9x?N$D^XSbygrVcwI7Pmc-3)$v;w^8l3*4GhMwtX-_i$8$YfC9m!8) z{Xz-gi}JsJIM~Yn{;~d=|M9y#@vOXwJUQuCw`ZlqNhIyvx@SEmD;CJ+H?8v=W60$# zgf~A_EA`6Z9uQt^1KWyWAwr?$90A)i%`gLQlxMw?mZd$O2N*ie#?9^mL^5;k8vw%F z+!rEgZW>^&iV3)a9LK{;ykHWZzcUR&FOu&sMY3i99Mf~5d6Y~M5BO0l&xCI5!yW*s z8I6woMFs$(VFw*Wd(`?K2ooz|Ti}NsWFHT^sN-1f9gvd{uWL8gO7ATMB`k0;{m>o2(cFjnLzBp zb7o^>o~*e7#7-v(P8xF`sY`i)@WM78L&zl9x#SmI&;&%o_ANiY)V}9-kp_zyRvtlOQ8AEcj(XZz3%7o+%Pu{jiN)!Rbh9F6E zPVJ}~i3raKW!Q7BP#S(dK^oUoqaB|FNaD>bgOVtV0L2y1){R{y#l&6XhI8_ zO-Rr;Cp)7skr+vf2_Khv$%2Af;p;%Oq>fNH%b0bTm>W7GJSiQsX}P7C6gS!PF_5|Z zyfR2US;g7o$I5QK2LRcgmG)i$a4mHG*~8Zj12n)=aqXrzAg2y!JqLqtk})UinR z;fwg1hH`Fg3qE|Y5Fv1Zz7XRPr2%k6zT_kD|GxdbCVl?rpTF0n!+-u>Wj{asUX>31 z`EK5=d`LFt4f7ZmW#$SKdtH0NzhF_>&J@~yQefsC0&|bgW5ye!# z<^(rPirih;Ea)c(xf8QNt^;hpH%t>PVEQ*6aIh;MW%M@X^~S46m+{r6@Ktcd1=l{J zuRsKs=tm&T)_(TQ?(YPEzMJYVa1`FDO#NRJ5)Q#vBjD8tcr^mPnGqnS zS{WN4CUmlABS#JEQ7ZdQjmn_NN>R@86gS`}xJ~;~px-9D`;_`@c<4I4YCh5+xa>XS zTVc~V+HV{;kq{gM16+tS=$fkO3k6Do9FxX$J~8O@Q4 zY;CJ=c@!}XdzEhjv1$K87K~VXH=9dL&sJ>u}CLmM22)f)Y6#KFSi>COS})Obgm8Irc*EcoIZLH zR%ux03T^I~FzkioA0td%7>bq+bO%Y=>K;59t9)7T;wfCZLu@j{R`f{V*Z<)G9cfrM z?8UDIWse=GEfKh9${II!3d)~A9e6e(8gz<_x(LkH6otqXRA@yvo4AmvywFVJwRn|o zcLZioTM-kIB`Q-|wGAG`!Tt&F_RXslW)dvbi$WzrJbpY`{zwW~GmK3H?VOSpd}#T& z4f*}CWxz61Y=P1yY`j#aJ_d4>li*{egmtBXoycX!0;0WK-!9*$cV^OCGUR#GJnl2i%+#4f4RvB%C8qEfN8Jk6&&Q zKq)GX!5Jph$B)%JQLj2ux8o<1PBZ&?I(tsHnEFSc2nGv)koK6`{t@ zkQ#Q`#znh4mXK4}#sMze#xJPelrKB+c4s2}bTDQ-I;;qU7*b#g)`${5L%=h@8FxW` zi6THt&t4%tk74|-6#_!qiU}UW@~l{Hi|CB*69JSPk`t`+pzPDO0fD%8MT)sMwyz||1C5p#pO9{67LCcVsDlfABaCkh$ z>A%Zzc4uHS-O&>~FS3+D6S@UURM`>X`tQqtD!ss#@7K?w=}WJ5=Z$Uv>s^ zZ>L6f>3i!~IsS609?PXlxBJaOtJ^;JIv4igvn}ZRKXzE?f4CI=mblm%=z! z{7bk0{u>UMNx)dhVnoeGzUdGWmG|KkA{Q(sQqQ6 z94qRmOuh5bp#Oz&(Xmpx_)dJsYmAy_?cQ;j;=SX3>sT4cH(!Rw@^P=Nv`f(|swTIu z)b>azJ(|6?ayBT{lbsL>3yWD)mV={e?cPwi`7!~fBw5=&zEpRtcsXbuDPt5Y6#R(cm!x$a%=AjMhfazeb6 zp7w05ITt|So*fDAZ00r=D4{!NZD)ZpTUwx)9=RRSAVFQ5AY0ht*)fq>dUog@kO-ow zJK12ur>qR+;h1N$2zAm;0lXO*b_m=aG z83`=zsZ``Hl!{z461XMUE&(U>SMa+GOBKv_IY_`UpJ0s@q@oa>`~?M43bob9SlB#i72%%f=Yd4_9J?B_ zkX^70lvth&n;@5tpoK;TMaNVG*F9rtG2uhbR~y@TVx4_1l>Mwfh3jZ;r=J-+1^_{t z=OTS@hg`-fq#5Scf8NNFfK3==*U>yd{LYp|yO*hh{(6_T|zvhOCSb?8+RD<1KqgDt5QZI z;F-RMm{2y=^Ub*(0A6{>U5_}uUJRxzA~IeKp6Z$}0qfbisjMn&uOO={6dB;3e~D4iJ~C0wj$lsf+=4pz~rpM6k%eh2F^Tc$iq zi4?bH2ImD{K&^^Asm}wpq?XxdtAJ@d!smPLENJVHVmISFYk4qaHHM~#y7nbbpX23$ zJzI6Oa>5F~0mn*U=o=B-%lEc3`r< zRvojx6sfwP{9+}Zk{kCv4kPM%JheazIX@5$1yks2{e$`WXwc7dpM7&|xS0A;1LNN> zix?G#;ta$Dx^|XcvL#YV$61`bMG7i(dbh4&qdE|r8mG8NO^nXnRPX3Bo zwA{2ZGvt--%j*VVZnj(j1fv)MRw4GZR}qH&0228^i= zNhwtXyKMJCl-ROF$sMQweBXG#@Z`@E02}0l+co6@m!Wb=E`A|qf37AOO%>_eXq3fl zQDV%BS)fdiOyO{UbH#lq^!A_E!LThte%`)Vh%WI@yb~<&fB6~vZF6$vO_8jYbKArY zvL)yk-$;Jt4ROgq{xZ_PL7Vkf1B;^w&l<@w{)Lpm2qG)auB3Q9|lO5S(pmo zvVk2T*9e_Uoy?Tq;wdsxG;){ob&g#t=QGdKn)BU))HU+rLwS;eqVG;#3JVE1Moz9E z36R_l(wwg)G=*;v?aRI1J^1hfT$lj$Vu21l-&B95<3hkP>=)~&59;Om>1UD|%wGaU zq1^gv-E)1V#pw-7nKxzJUXa4)ojro4vxJiES;#fCoN_b6hHjRf&I=TnR1(mUW>~3N z+VL zGGNKJ?eAtiXUuk3VDow!B6W$ig3aQ>h1SknJ;jN-0#%>qD$6NTctY5LbeZ#+$CEy1 z4BcIqu7YD-{*n4nT0Kujz3$fJmV35XAQVzVE1J4x10J6|(Y{j{2|pM4y3Ax3dx_mY z4?WYm$pH@vv#|%I^tBBwLo1QnAShWn$AUN&ms8-D8 zK#(gLw&Ud$da$0M^ghFf8k{?j9GG!a#PghxxA5VZA1r>Z(T!XxIoR>CUCojoVV+aK zMLJKK{TwmIx+E^0YNlUkxHGhN(TZ2flX?puhKxQvaa-WCv66jS}ABw3#^2U;eXx@#n1$lzTwv5 zkJ~5Tws3#up>pq#D^a8^ZOp{SH_*RC8%of`I)BHuNRpo;du7;pl4!JqE*fGZS0Ns_ zP&4_pRl&_u{-u36b%txY%+>3Cpj};DeFl2ahWR@mBZpr+c!G1qdlP%wvUQRN=jzg+ zd9w5j+PaBmFegvH1bQA}CS@KRp3sT0R8E;Gva^kJ2{0*}hlOhE$R!$fQROzTT-%x< zH^0bKl}|tid$zuPRc5jzn&(3rdj;62-HTlBg>o|-G(fYLod}vy<`2(s(RqFun5z)> zFjg?mU8*mk3-5Ac3{G9>EU&WDuS(z)Dczu6HQ)s`#Kl4{9#9kZwS5E_->>@}zwXMr*>f z0ww}3@xsuG*Uw8#Jpx+TGVDA&d+Yp>602+;07G*z!E?%%#S+WY#CXLe z-7(K}SjeyY{pRRcTozNh=B5o&$3a>9@19UQ3|kVw(bH-uR>e?eK1O@U}A?wevW<+szZ`6EtfbkMq$h zki;A*6H&5%o?G_z>Uo=P&$2#}+VoFsdjxx8k}LEcE9;~}iIfa7x-%FTwID^*$Tl#7 zq$N6}NMZ$_)-fJC@!i{`F!QMQ%M8RfNo8JeJ5m|Pa>1=gWk5Xd*kKb}7PNIBqQRU}R7PgsRDmy;3!!i#5Bpjvh+a>)L|b7>bt&uDGNvQ*kf~JK42wMIzR6 zHHVNcff8K*S+b6cbVK9ecR&tq)(^rp%7KuPg((6Z!wl7NOSl5wWoa80Hi%VmXWtvGKuTM+^^|Cg|USys6Z=m!1)E5=Rd+~E$14D8r$+Og+ z!WgohqTll3#)>1xo?~y|IlYH28jEXB)3C@L0E#Tfgz4Gvi-?$~vpr|j>C#1zN*c5r zXe`o`F+FSS4e}!K6hCf{OHiok6X@ri-=6yuCoA+aGM_)s8hiQEbuX31=bIkWDG`8K z>c~WpqwWOj&&^Z!8itx#?Ac+cv12o;Yre!;8GhdX=^ljRW}Z*`q&XO7-;gSSlUDn( zt>kSyob(3c-0wqqFzj^u*;fquWroK}Gy7UZ2{g~z&5LgK4UT?^ftu$BZVvkWb~C?3 zE6O_qccEVBx!Zs|I zE0V%eg%dwfGJpODMT-`tM7g}BDJ)p9Kt<(B5U?cu@|LWiOzy%J6v`CWpX^^)V24xDC#IRV2 zg)v^XaCr<07c7!tvBD)ND4D-t#!D6Rd%@xvD^_OqlvzlFvSpXkpnSPSRXpF_^jcz_ z1Pk#?8DKAQiH|`IY(sOH8^HZM1OBUzxh|1=sL5mm6 zPly&hJ8{F4cV9N!D@*j96Q@17p;F1zo)0}Q!P--y7bjhNI^?1g+E0aCken@g%1eoY zgl+Mnd8yl?XQ!!AGM(Ed^s;mRsVUwz+bcUhCDXn=wL4|ktDS~m$MkT~Q)tIU&#nve z4%?~Gv)fRKr|cH%W-F!jfE_h~GG2h_**)>x6Z^4KGP>y5fkU38UX`QL4oT~xX9qPa zCy`yeTtP~^c+ukIcJa~$Y3|}ha}(ah3+AW3i=Lf^ZHIJtPi$eA6nUFXlocGiq|J+- zZz?QHCe@3cr-;fO676j^R9;p8rsA?(LcUGdC0Q>&NniAo7c7N|`{IQQQ}}IQTDDue zruB=SSM}gW^LvSefzh+Wf>)|>P%@ohynNZjf@yV1rWlM@El4&PFPfWnFkZ0qK$cBK z7_U(>DPg=yiS&f=;(6(wqGtytFU{VbYmU&uLN?{c^2$Yie79V#j*BVQ@G}yl=iZ|u zK0o6u*=si6f^yI1fc*9S-y{jI=bFug*Zg}hfv`WyY+#m_8v^9OE#XAl5oW2qiqbI4 zIAlr#E2BY{3s8fOW3Gc`LL$XyRg)FD-oTGoJ^@7F>@F^dB^%Ra@e)$r9+tr3zG$5* zwU(Dz2)}RWM<5*v=EOHWuy(&RC^8K~CBu37<1;)HfsFp}5Xrk$vpARVJzVD<=(bUQ z-WUg)hPIZ9Qi+xre#1G|br+Gm%jScVPQU(Ml|KLTU$ys*zy4>X`1iBIc@3urNtw(4 zz8_LA!IfR-=RJ`2V40GWvtITE`!U4Ac3E zyOQxK<@Ls^J3rG6xU0<&g|+Av@1!KAbL^|1lv9soIU^LiaDn_ce|vp?uE$;&_Jd^D z3l|Zxuemze7ZHdWw&%ve6S`Jq4K4G@ZB(n(>W6~^{(rSv-Tc4tpp!G>6lI`frYlmRv?jaQWXg&JAS^&IW!5wI#PZ_E$krsc-S2`gmM?G zO>zA*4FjOFK}7`6B})D7`h#CdUsJNy${3@?)~EzFzcO@1nFJR8h$0;j-W7o7fG!OJ zT*PT&)9pJi@Ekzf$Ep(Kp=Xr!Or5E5cgUr{MH*h%#$yPX0E&5&CZvcY*IC0B-X;;o z@g6{12Nqld7qK>gXUCxNBBTO(*hRpw=lDo$C?D%brY`qc#f z5qa4wMisd|Qo`})Ly)97r*>40M1*IAGVD2W#lX#7Y;lOl)*Wovl>ecb#%34=@n)7m z*+`3h2taWKG@)%{3T6m+jt(i(E1+wGCbW>*gamzavNH-3iIKFJ@Nv1AtFHx8Ep>!a z=HbXe8XGf4I969oiqt8apy=lG%|s|$f&=ToS7N>T?m#w_iYU`)4s`ZWqI(g{ip_8{8Cr>U`n_t&1<=%ZG zH{AnC%b2D0>AJlQfJj<`Z^+|{m5eYNt>jP22og%-z0(WnBqxTTQavoJmgk-Lzr z6L-&Dbx$cSdsC96;>SrR-?A>bqvf_`7_*c*UNY8V<@CPRpG3lWpMJO7hs%$EAK9eQNIf2xvZyvWN+zU{ULf;{-o zyW{uohUb6BCy4*-MCQYP2Gi(m;XuR24e+4BpA}%}ru&P+m%E7&2@2>L0HQ(d8UQo= z8=mta1cVUKZXuw-8nj_SzNKod+=arJGExWdi#)~$*v%!K@BW>2qil*H{(F$;f>T9* zzWDF`@$q7q|9Nt<_#WfGUC4a#pEiwNF!~!G_wjn!d)}vQDO%P1l>GO9dRQnsM>|m0 z!bQC=2En5CM@vBX00;*zG+_pNk!I-X1w{I=BXuPSr@;uSVUMfidfP7WBDYsb|`#% z+Lw0D)lb)-KV8ylbxMw89pXPsY2wGc4d^ynsFp2QbhMIBb_Fjk{l#RJto`u#LmSob zfBgQRM@gaRx@=7@u{ye3en@+r~1-sO| zX#+KxcGG_uNXq`M2}1jXlP9r9%lBAD@&*!)UFN1MMz-r{k$|Y4 zgEs%3w42Njm%$V$&;_u_ZxwMsUup$z!ml#_;^62uwC=Jgp>(&b<2LU@EY!LlBjIASG+lu*UlI9?;&Uj2|`Q=4C5kUEdc_!?2Q?D{vg&n(}nO;!OVEPznFR%q+tsp@@RiL{D0{*1T z0{^K-2Lx5&0aZ94C}nn7P|Eh8gOnYHry+j6YFv158UP)t6@`QqQUZeuDRY2{YBK@{ zQ_8p@l@g8xNEwL;NEv|(jIMPA>?mbE@KMTi5Tuk{f>m%(QnhXG)@Ue9DP>-;Qp#w^ zQpyW$CSyQMDP!LjQr`R~MgoCOwW1xZx{<9413=ZP_PbqU9j5(lRUlwGx}Fg5bW|l! zAXQ2joC*cD_c3aM;&Q4%P;^cT5OP&32pCle{3;~~gasPBf8z-SZ&ibWR_go{1`6t` zHGrD%$dbDi7{zpuF=eu-} zCh`6Fs9|6pQ5PJ)*$9Sg;D%z?a_Ernn@rcXbnV_F2$F)j>Sz4sV251Hn&Xa4%; ztYA(3Y+ZcbF?UT#|1@NoE2+}$rHvr*)HMC>h;oU=Ixxvjen=`{f^~!^Z|F z`8BC3|F-77U6^tUZ<4CQJ-6(Ku23Vad_+(aN=D;JmMdr3M2ZCegZMIND?J9k_gReZ zV~z9L<_o)GTKDUiKvyViefsuC0@x$(87pX-JUCye_6d|O57(?(d=kEglO2xpBQK=J$V?8^pDZRX8*34ZC^JovcELX_sdNF zYs2HUFSFS_UWm8q-cq;S1pQmGFIU666?+vx$?8mHIZyKJ-0>--p7NnS?{afn#kMyg zE4iSdv?cRQFg4wse3vi)z<7 zhp!bp%^weWMcuofeX`d}&Qcatxy70E&N`K!%s2l$##kMmEu5YC_uFwRqUD_Xjir$~ z5s-LR+gfCC(Z&r*AkC5q=z%tf%%+9cJ`B`r?Yy`Y{5yg+jntfpPkY)R-86&pjxcPq z)y}rNd!TJhF*3ij48Hhr%{ZrbaF(Q!QNEc1XnkCe>p7o(X#z`>t;EOwDruOuBY}1E zzn;8boGb?XukTOZBme78WJmn3k}>S7BY_oHxQjm;nP6qFNTp^v*s@n(;)E@?BZW25 z!W!QNfz+__;(8Z9?7+GCW=e)w7e}mtCDzzr3sdX{SFE?Nk1y857`u(qqSrFyjqS9? z&m%oCcdWr-LfTiPeMQ<=A){$u17on!{Ha0QuFYE!NkD-X`K=;I0y-lJs8iwIQWzo$ zsM|y&0qq1yK+WUh+xDOK%>BU@l7Masl7LQWCz61UR)!>?&w?bN zNCG+(Bmwm)LK09#kOb6hAd-L%6e0<<;C~gZEP*38*fw z{MIQX0o|jO6fldX1*N6nty4~UGg}J5Tc>>b@zyDe;H^`5>(s>KgSSq-m0PEf1k@r( z0y?QiBmqSd(9Ne`9SJDVL)$ZwlwDAT2E;zoUvV#Ghe0e-g-V(BdTuF5E$WjuM=ct( zc}#wtzLZ@;V6UB8bmW?z?BabXb6mgQYJQo`1gS-5tQDz6^{V!}T}UlDwjQJwHOUU5 zETk4iYEh&X4Ko_4MeXJzwJ1`HVrnL)W@2h4!PHEv(U_WvshOCXiK&^oU}|PBn40O; zkExjs!PHF6U`)+~RFA2d?nf~-b6!GFBY3uMto=@--q~quE&GYec?Ai`mH6CayNQN)U)R` z%U)6##+0q|jI(C*6!W7uQ?h=F(wbMSJW8VC^pEcri*N7i zB|GF#ykhHbuAN|I%Nl%s`E0bl8B~fahzwoacLbo zUuyh~>%SSM2{!=W`ak~eyW#r(!!fS^oru2v!K)pzoL{C=U6500030|B3N} IlmHF`0Q@c+D*ylh literal 0 HcmV?d00001 diff --git a/files/tests/samples/valid-helm.tgz b/files/tests/samples/valid-helm.tgz new file mode 100644 index 0000000000000000000000000000000000000000..5df17ff775dfbdcb3d51b5404b66fd42a36de0e3 GIT binary patch literal 27436 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 Date: Fri, 31 Jan 2025 14:13:15 +0530 Subject: [PATCH 14/33] add support for identifying kustomization Signed-off-by: aabidsofi19 --- files/identification.go | 72 ++++++++++++++++++++++++- files/sanitization.go | 114 +++++++--------------------------------- 2 files changed, 89 insertions(+), 97 deletions(-) diff --git a/files/identification.go b/files/identification.go index 660804e9..fb5fc92b 100644 --- a/files/identification.go +++ b/files/identification.go @@ -5,15 +5,18 @@ import ( "encoding/json" "fmt" "io" + "os" "path/filepath" "strings" - // "github.com/fluxcd/pkg/oci/client" "github.com/meshery/schemas/models/v1beta1/pattern" "gopkg.in/yaml.v3" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" k8sYaml "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/kustomize/api/krusty" + "sigs.k8s.io/kustomize/api/resmap" + "sigs.k8s.io/kustomize/kyaml/filesys" // appsv1 "k8s.io/api/apps/v1" // corev1 "k8s.io/api/core/v1" @@ -31,7 +34,7 @@ const MESHERY_VIEW = "View" type IdentifiedFile struct { Type string - // pattern.PatternFile,[]runtime.Object ,*chart.Chart,etc + // pattern.PatternFile,[]runtime.Object ,*chart.Chart,resmap etc ParsedFile interface{} } @@ -39,6 +42,15 @@ func IdentifyFile(sanitizedFile SanitizedFile) (IdentifiedFile, error) { var err error var parsed interface{} + if parsed, err = ParseFileAsKustomization(sanitizedFile); err == nil { + return IdentifiedFile{ + Type: KUSTOMIZATION, + ParsedFile: parsed, + }, nil + } + + fmt.Println(fmt.Errorf("Identify Kustomize errors %w", err)) + if parsed, err = ParseFileAsMesheryDesign(sanitizedFile); err == nil { return IdentifiedFile{ Type: MESHERY_DESIGN, @@ -158,6 +170,15 @@ var ValidHelmChartFileExtensions = map[string]bool{ ".tar.gz": true, } +var ValidKustomizeFileExtensions = map[string]bool{ + ".yml": true, // single kustomization.yml file + ".yaml": true, + ".tar": true, + ".tgz": true, + ".gz": true, + ".tar.gz": true, +} + // ParseFileAsHelmChart loads a Helm chart from the extracted directory. func ParseFileAsHelmChart(file SanitizedFile) (*chart.Chart, error) { @@ -179,3 +200,50 @@ func ParseFileAsHelmChart(file SanitizedFile) (*chart.Chart, error) { return chart, nil } + +// ParseFileAsKustomization processes a sanitized file and returns a Kustomize ResMap +func ParseFileAsKustomization(file SanitizedFile) (resmap.ResMap, error) { + // Validate file extension + + if !ValidKustomizeFileExtensions[file.FileExt] { + return nil, fmt.Errorf("invalid file extension %s", file.FileExt) + } + + var fs filesys.FileSystem + var path string + + if file.ExtractedContent != nil { + // Check if ExtractedContent is a directory + // If it's a directory, use it directly with MakeFsOnDisk + fs = filesys.MakeFsOnDisk() + path = file.ExtractedContent.Name() + + // Ensure the path points to the directory containing kustomization.yaml + kustomizationPath := filepath.Join(path, "kustomization.yaml") + if _, err := os.Stat(kustomizationPath); os.IsNotExist(err) { + return nil, fmt.Errorf("kustomization.yaml not found in extracted directory") + } + + } else { + // ExtractedContent is nil → Read from file.RawData (single-file case) + if len(file.RawData) == 0 { + return nil, fmt.Errorf("file is empty or not extracted") + } + + fs = filesys.MakeFsInMemory() + path = "/kustomization.yaml" + err := fs.WriteFile(path, file.RawData) + if err != nil { + return nil, fmt.Errorf("failed to write raw data to in-memory filesystem: %v", err) + } + } + + // Use krusty to build the Kustomize resources + k := krusty.MakeKustomizer(krusty.MakeDefaultOptions()) + resMap, err := k.Run(fs, path) + if err != nil { + return nil, fmt.Errorf("failed to build Kustomize resources: %v", err) + } + + return resMap, nil +} diff --git a/files/sanitization.go b/files/sanitization.go index 343234f6..598c9d21 100644 --- a/files/sanitization.go +++ b/files/sanitization.go @@ -104,10 +104,12 @@ func ExtractTar(reader io.Reader, archiveFile string, parentTempDir string) (*os // Create a tar reader tarReader := tar.NewReader(reader) + // Track the top-level directory in the archive + var topLevelDir string // Iterate through the tar archive for { header, err := tarReader.Next() - // fmt.Println("Header name %s", header.Name) + if err == io.EOF { break // End of archive } @@ -118,6 +120,11 @@ func ExtractTar(reader io.Reader, archiveFile string, parentTempDir string) (*os // Construct the full path for the file/directory targetPath := filepath.Join(tempDir, header.Name) + // If this is the first entry, determine the top-level directory + if topLevelDir == "" { + topLevelDir = filepath.Dir(header.Name) + } + // Ensure the parent directory exists parentDir := filepath.Dir(targetPath) if err := os.MkdirAll(parentDir, os.ModePerm); err != nil { @@ -145,7 +152,17 @@ func ExtractTar(reader io.Reader, archiveFile string, parentTempDir string) (*os } } - // Return the temporary directory as an *os.File + // If the archive contains a top-level directory, return its path + if topLevelDir != "" { + topLevelPath := filepath.Join(tempDir, topLevelDir) + extractedDir, err := os.Open(topLevelPath) + if err != nil { + return nil, fmt.Errorf("failed to open extracted directory: %v", err) + } + return extractedDir, nil + } + + // If no top-level directory is found, return the root tempDir extractedDir, err := os.Open(tempDir) if err != nil { return nil, fmt.Errorf("failed to open extracted directory: %v", err) @@ -165,99 +182,6 @@ func SanitizeBundle(data []byte, fileName string, ext string, tempDir string) (S }, err } -// // will skip nested dirs right now -// func SanitizeBundle_(data []byte, fileName string, ext string, tempDir string) (SanitizedFile, error) { - -// var tarReader *tar.Reader -// input := bytes.NewReader(data) - -// if strings.HasSuffix(fileName, ".gz") || strings.HasSuffix(fileName, ".tgz") { -// gzReader, err := gzip.NewReader(input) -// if err != nil { -// return SanitizedFile{}, fmt.Errorf("failed to create gzip reader: %w", err) -// } -// defer gzReader.Close() -// tarReader = tar.NewReader(gzReader) -// } else { -// tarReader = tar.NewReader(input) -// } - -// // cleaning up is resposibility of the tempDir owner -// extractDir, _ := os.MkdirTemp(tempDir, fileName) - -// // Extract and validate files in the bundle -// for { -// header, err := tarReader.Next() -// if err == io.EOF { -// break -// } -// if err != nil { -// return SanitizedFile{}, fmt.Errorf("failed to read tar entry: %w", err) -// } - -// target := filepath.Join(extractDir, header.Name) -// // Handle directories -// if header.FileInfo().IsDir() { -// if err := os.MkdirAll(target, os.ModePerm); err != nil { -// return SanitizedFile{}, fmt.Errorf("failed to create directory: %v", err) -// } -// continue -// } - -// switch header.Typeflag { - -// case tar.TypeDir: // If it's a directory, create it - -// if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil { -// return SanitizedFile{}, err -// } - -// case tar.TypeReg: -// ext := filepath.Ext(header.Name) -// if ext != ".json" && ext != ".yaml" && ext != ".yml" { -// continue -// } - -// // read the complete content of the file h.Name into the bs []byte -// // fileBytes, _ := io.ReadAll(tarReader) // Validate the extracted file - -// // if _, err := SanitizeFile(fileBytes, header.Name, tempDir); err != nil { -// // fmt.Printf("Skipping invalid file: %s\n", header.Name) -// // continue -// // } - -// // Create a temporary file for the extracted content -// tempFile, err := os.Create(target) - -// // fail forward and continue -// if err != nil { -// fmt.Println("failed to create temp file: %w", err) -// continue -// } -// defer tempFile.Close() - -// // Write the extracted content to the temp file -// if _, err := io.Copy(tempFile, tarReader); err != nil { -// fmt.Println("failed to write to temp file: %w", err) -// } -// } - -// } - -// // Return a handle to the temp directory (or a specific file if needed) -// extractedContent, err := os.Open(tempDir) - -// if err == nil { -// return SanitizedFile{ -// FileExt: ext, -// RawData: data, -// ExtractedContent: extractedContent, -// }, nil -// } - -// return SanitizedFile{}, fmt.Errorf("Failed to open extracted dir %w", err) -// } - func IsValidYaml(data []byte) error { var unMarshalled interface{} return yaml.Unmarshal(data, &unMarshalled) From 533b3c21df5578a756f08d8d09134b3034b7d05c Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Fri, 31 Jan 2025 14:13:27 +0530 Subject: [PATCH 15/33] add tests Signed-off-by: aabidsofi19 --- files/tests/samples/wordpress-kustomize.tar.gz | Bin 0 -> 2363 bytes files/tests/sanitization_test.go | 6 ++++++ 2 files changed, 6 insertions(+) create mode 100644 files/tests/samples/wordpress-kustomize.tar.gz diff --git a/files/tests/samples/wordpress-kustomize.tar.gz b/files/tests/samples/wordpress-kustomize.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..d60e5dc6825ddd514742b5c1d08088e46bf180a7 GIT binary patch literal 2363 zcmV-B3B>jviwFP!000001MM4YbK1t%pYnKYuc}Kj6k}=5>M0=RxDP zsomI~)rB)Uci76M9@kvTiA6Z+Y5*Q_X%}L#(SkWOMLVbN+##Hb{{f;_0k&uJ*UaO@ z_Hd=BRSz+VUfpD_OFS#=!YMAf#y*QqY~sbO;ixYBx7L{0UL%;*evdnee;tS(-K-r! zjhZtCPo91p49_ly{n6;ts6YJh?sfl%A0h0z_;vKldE6W$GB*;MY&lwzdbi#9AsUt} zdG_kl+rj869szJmoCOZaFTNee<;&4XH01HT@tNCAH6qig=~MnQT@C3p+Y=p|y9z!# z`kbUTq2PwDx**!cF15!N$_n-diT#(d@gPya{r&&Ram%Ru|2`mp{F!G;WFCaO_DVy@Xm|EgLJvjOWi+ zXSUykKmV$n%SOoC{~NT2%(eeVIO_h6TtHd;XLOEp_P-;3D*L|&z@uji-xLa_L(chQ zOFA8pkHP|}r^lwef?dFR{uiI{&ekZ+|BSr;w_8S~|9gQA`JWJiO_7|6FI2=en3K;7 zszsv?F%X=Rg~Q`m1MC25#Byz~Ru_q3*xZF*KMpzh*m_afkw=9@|CbN;Ch-3CzgF|O zdjGc;EvIJ$cOw->l){V)9zY%QH%e<@f{2YhA}R{_(Qm2Wv`#Ck;> z;+fR1yu<@Rk^b+x23S`AYo4Cu^FPc0EB)UG?4$m7Por_SI$#>QS365tP;7q`fRdvb_oykH23fZn<1GX51|)#N7#bRgC*qp~(L4x&~NU z|7*AM_P=vnz5m?{+^7Ec*tAx#dq}i?*nfF;(XYGPl;G>2ME_CoJNf%RC!J=c|N8(@ zv2$4$-g$pu&}DnqaPuqxA0q+Vr6A%YuszOFVUMQ0^MKFn0QdskyiJ@VSg{2xZN~wJ z65oRvTLR7zvEn##CUh*CENVIggGmAyVc2BiQ1I!L`V^BqAf5%mViIs0F;lQPE}bm= zeGpv$t;2hU{d`9F5&osgB=lFJ;OH?fLs;TZYzz(k6-;gFSizCR836N~m5B2k+ms}v z_b1-h9?9FJlAd@;O}k=(FYpmH=2B>kz=lM56s>0<7U4|OWc-FrJQ;_a;WOOv#cK4+ zxn${t1QbUSQ_NI5PwdddI zI{YO1#2LS3i25f^1OD3{= z2Ti@OR_*bS@I|eAW7$6Fb9j=B(vmx@`AJFmnoP`&mV{`BhbWwE={by85c?@ji`xX_ z;&ypjc78wQJZl-54w$pPnPv-Jl(}am;hjh5IP8`1EH{|3 z!Qo8w1-8#Tu}tEygx>+;I5;_>2hmM!&q|?-4Sh~kE+wVkf|}$NB$19y`XRZTmi~nG zGZNqt39g7BBBW01ff&_;qR&ivuvRH8{)!>xLlSzZvF@*8MFB_loCAgMYT0t>Z)40 zIe%F#+gI>9IX@tKVNHpV5H@jcMKK||ug?n`xq~CnQn<=GN>xQuK|M=EeJZvM%^a|a z2zles(5HgAD23F5$x14+jHhPIvCv*b>K|xDCoAoBo}qPR{a!_O;X{N%SPMbQKvD2U zQV`h*Vp%v4xpqI>USiyld|A3%KiQC4S*t^q_{rVE?8~6^!R0HExwlB7NwPJCOg<|& z5$SkcTH5@j$giO%ePJ+1OXP^gYdao`R6s`7;&3E==yKDvjYTLPt_LaaT!(j4SYmp_ zyzjX*Joa=2<7mrcm6cfGRR~-qhqewQ2II_j?@Npd4MGVUrScyQt=`o36`t5+JR&Jdw_iyqdpd{$u7UHib1fiGN=3~+pechfHd6%3$-{dUrf;e##Ii&x5 hJ+7dF3M#0ef(k0Apn?i2sGx#v@PGd2_>urn003hew&nl; literal 0 HcmV?d00001 diff --git a/files/tests/sanitization_test.go b/files/tests/sanitization_test.go index 0e276c44..88155819 100644 --- a/files/tests/sanitization_test.go +++ b/files/tests/sanitization_test.go @@ -85,6 +85,12 @@ func TestSanitizeFile(t *testing.T) { expectedExt: ".tgz", expectedType: files.HELM_CHART, }, + { + name: "Can Identify Kustomize archive", + filePath: "./samples/wordpress-kustomize.tar.gz", + expectedExt: ".gz", + expectedType: files.KUSTOMIZATION, + }, } tempDir, _ := os.MkdirTemp(" ", "temp-tests") From 16b8c41d05bcd4d4c83ade48fc86fa219e582025 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Fri, 31 Jan 2025 16:43:14 +0530 Subject: [PATCH 16/33] add support for zip Signed-off-by: aabidsofi19 --- files/identification.go | 68 +++++++-- files/sanitization.go | 145 ++++++++++++++----- files/tests/samples/valid-docker-compose.yml | 37 +++++ files/tests/samples/wordpress-kustomize.zip | Bin 0 -> 4514 bytes files/tests/sanitization_test.go | 14 +- 5 files changed, 211 insertions(+), 53 deletions(-) create mode 100644 files/tests/samples/valid-docker-compose.yml create mode 100644 files/tests/samples/wordpress-kustomize.zip diff --git a/files/identification.go b/files/identification.go index fb5fc92b..8dd1c07a 100644 --- a/files/identification.go +++ b/files/identification.go @@ -23,6 +23,9 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/kubernetes/scheme" + + dockerLoader "github.com/docker/cli/cli/compose/loader" + dockerTypes "github.com/docker/cli/cli/compose/types" ) const MESHERY_DESIGN = "Design" @@ -34,7 +37,12 @@ const MESHERY_VIEW = "View" type IdentifiedFile struct { Type string - // pattern.PatternFile,[]runtime.Object ,*chart.Chart,resmap etc + + // pattern.PatternFile (meshery-design), + // []runtime.Object (k8s manifest) , + // *chart.Chart (helm-chart), + // resmap.ResMap (kustomize), + // dockerTypes.Config (docker-compose) etc ParsedFile interface{} } @@ -42,15 +50,6 @@ func IdentifyFile(sanitizedFile SanitizedFile) (IdentifiedFile, error) { var err error var parsed interface{} - if parsed, err = ParseFileAsKustomization(sanitizedFile); err == nil { - return IdentifiedFile{ - Type: KUSTOMIZATION, - ParsedFile: parsed, - }, nil - } - - fmt.Println(fmt.Errorf("Identify Kustomize errors %w", err)) - if parsed, err = ParseFileAsMesheryDesign(sanitizedFile); err == nil { return IdentifiedFile{ Type: MESHERY_DESIGN, @@ -72,6 +71,20 @@ func IdentifyFile(sanitizedFile SanitizedFile) (IdentifiedFile, error) { }, nil } + if parsed, err = ParseFileAsDockerCompose(sanitizedFile); err == nil { + return IdentifiedFile{ + Type: DOCKER_COMPOSE, + ParsedFile: parsed, + }, nil + } + + if parsed, err = ParseFileAsKustomization(sanitizedFile); err == nil { + return IdentifiedFile{ + Type: KUSTOMIZATION, + ParsedFile: parsed, + }, nil + } + return IdentifiedFile{}, fmt.Errorf("Unsupported FileType %w", err) } @@ -168,6 +181,7 @@ var ValidHelmChartFileExtensions = map[string]bool{ ".tgz": true, ".gz": true, ".tar.gz": true, + ".zip": true, } var ValidKustomizeFileExtensions = map[string]bool{ @@ -177,6 +191,7 @@ var ValidKustomizeFileExtensions = map[string]bool{ ".tgz": true, ".gz": true, ".tar.gz": true, + ".zip": true, } // ParseFileAsHelmChart loads a Helm chart from the extracted directory. @@ -212,14 +227,16 @@ func ParseFileAsKustomization(file SanitizedFile) (resmap.ResMap, error) { var fs filesys.FileSystem var path string - if file.ExtractedContent != nil { + if file.ExtractedContentPath != "" { + path = file.ExtractedContentPath // Check if ExtractedContent is a directory // If it's a directory, use it directly with MakeFsOnDisk fs = filesys.MakeFsOnDisk() - path = file.ExtractedContent.Name() // Ensure the path points to the directory containing kustomization.yaml kustomizationPath := filepath.Join(path, "kustomization.yaml") + + fmt.Println("Kustomization Path %s", kustomizationPath) if _, err := os.Stat(kustomizationPath); os.IsNotExist(err) { return nil, fmt.Errorf("kustomization.yaml not found in extracted directory") } @@ -237,6 +254,7 @@ func ParseFileAsKustomization(file SanitizedFile) (resmap.ResMap, error) { return nil, fmt.Errorf("failed to write raw data to in-memory filesystem: %v", err) } } + fmt.Println("kustomization extraxting from path %s", path) // Use krusty to build the Kustomize resources k := krusty.MakeKustomizer(krusty.MakeDefaultOptions()) @@ -247,3 +265,29 @@ func ParseFileAsKustomization(file SanitizedFile) (resmap.ResMap, error) { return resMap, nil } + +// ParseFileAsDockerCompose parses a Docker Compose file into a types.Config struct. +func ParseFileAsDockerCompose(file SanitizedFile) (*dockerTypes.Config, error) { + // Parse the YAML file into a map[string]interface{} + var rawConfig map[string]interface{} + if err := yaml.Unmarshal(file.RawData, &rawConfig); err != nil { + return nil, fmt.Errorf("failed to unmarshal YAML: %v", err) + } + + // Use Docker Compose's loader to parse the raw config into a types.Config + configDetails := dockerTypes.ConfigDetails{ + ConfigFiles: []dockerTypes.ConfigFile{ + { + Config: rawConfig, + }, + }, + Environment: map[string]string{}, // Optional: Add environment variables if needed + } + + config, err := dockerLoader.Load(configDetails) + if err != nil { + return nil, fmt.Errorf("failed to load Docker Compose config: %v", err) + } + + return config, nil +} diff --git a/files/sanitization.go b/files/sanitization.go index 598c9d21..be867d25 100644 --- a/files/sanitization.go +++ b/files/sanitization.go @@ -2,17 +2,15 @@ package files import ( "archive/tar" + "archive/zip" "bytes" "compress/gzip" "encoding/json" - // "path" + "path" - // "errors" "fmt" "io" - // "io/ioutil" - // "mime/multipart" "os" "path/filepath" "strings" @@ -24,7 +22,7 @@ type SanitizedFile struct { FileExt string RawData []byte // incase of bundle like tar - ExtractedContent *os.File + ExtractedContentPath string } func SanitizeFile(data []byte, fileName string, tempDir string) (SanitizedFile, error) { @@ -82,30 +80,22 @@ func SanitizeFile(data []byte, fileName string, tempDir string) (SanitizedFile, } // ExtractTar extracts a .tar, .tar.gz, or .tgz file into a temporary directory and returns the directory. -func ExtractTar(reader io.Reader, archiveFile string, parentTempDir string) (*os.File, error) { +func ExtractTar(reader io.Reader, archiveFile string, outputDir string) error { // Open the archive file // Determine if the file is compressed (gzip) if strings.HasSuffix(archiveFile, ".gz") || strings.HasSuffix(archiveFile, ".tgz") { gzipReader, err := gzip.NewReader(reader) if err != nil { - return nil, fmt.Errorf("failed to create gzip reader: %v", err) + return fmt.Errorf("failed to create gzip reader: %v", err) } defer gzipReader.Close() reader = gzipReader } - // Create a temporary directory to extract the files - tempDir, err := os.MkdirTemp(parentTempDir, archiveFile) - if err != nil { - return nil, fmt.Errorf("failed to create temporary directory: %v", err) - } - // Create a tar reader tarReader := tar.NewReader(reader) - // Track the top-level directory in the archive - var topLevelDir string // Iterate through the tar archive for { header, err := tarReader.Next() @@ -114,27 +104,22 @@ func ExtractTar(reader io.Reader, archiveFile string, parentTempDir string) (*os break // End of archive } if err != nil { - return nil, fmt.Errorf("failed to read tar archive: %v", err) + return fmt.Errorf("failed to read tar archive: %v", err) } // Construct the full path for the file/directory - targetPath := filepath.Join(tempDir, header.Name) - - // If this is the first entry, determine the top-level directory - if topLevelDir == "" { - topLevelDir = filepath.Dir(header.Name) - } + targetPath := filepath.Join(outputDir, header.Name) // Ensure the parent directory exists parentDir := filepath.Dir(targetPath) if err := os.MkdirAll(parentDir, os.ModePerm); err != nil { - return nil, fmt.Errorf("failed to create parent directory: %v", err) + return fmt.Errorf("failed to create parent directory: %v", err) } // Handle directories if header.FileInfo().IsDir() { if err := os.MkdirAll(targetPath, os.ModePerm); err != nil { - return nil, fmt.Errorf("failed to create directory: %v", err) + return fmt.Errorf("failed to create directory: %v", err) } continue } @@ -142,44 +127,124 @@ func ExtractTar(reader io.Reader, archiveFile string, parentTempDir string) (*os // Handle regular files file, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) if err != nil { - return nil, fmt.Errorf("failed to create file: %v", err) + return fmt.Errorf("failed to create file: %v", err) } defer file.Close() // Copy file contents if _, err := io.Copy(file, tarReader); err != nil { - return nil, fmt.Errorf("failed to copy file contents: %v", err) + return fmt.Errorf("failed to copy file contents: %v", err) } } - // If the archive contains a top-level directory, return its path - if topLevelDir != "" { - topLevelPath := filepath.Join(tempDir, topLevelDir) - extractedDir, err := os.Open(topLevelPath) + return nil +} + +// ExtractZipFromBytes takes a []byte representing a ZIP file and extracts it to the specified output directory. +func ExtractZipFromBytes(data []byte, outputDir string) error { + // Create a bytes.Reader from the input byte slice + reader := bytes.NewReader(data) + + // Open the ZIP archive from the reader + zipReader, err := zip.NewReader(reader, int64(len(data))) + if err != nil { + return fmt.Errorf("failed to open zip reader: %w", err) + } + + // Iterate through the files in the ZIP archive + for _, file := range zipReader.File { + // Construct the output file path + filePath := filepath.Join(outputDir, file.Name) + + // Check if the file is a directory + if file.FileInfo().IsDir() { + // Create the directory + if err := os.MkdirAll(filePath, os.ModePerm); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + continue + } + + // Create the parent directories if they don't exist + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + return fmt.Errorf("failed to create parent directories: %w", err) + } + + // Open the file in the ZIP archive + zipFile, err := file.Open() + if err != nil { + return fmt.Errorf("failed to open file in zip: %w", err) + } + defer zipFile.Close() + + // Create the output file + outFile, err := os.Create(filePath) if err != nil { - return nil, fmt.Errorf("failed to open extracted directory: %v", err) + return fmt.Errorf("failed to create output file: %w", err) + } + defer outFile.Close() + + // Copy the contents of the file from the ZIP archive to the output file + if _, err := io.Copy(outFile, zipFile); err != nil { + return fmt.Errorf("failed to copy file contents: %w", err) } - return extractedDir, nil } - // If no top-level directory is found, return the root tempDir - extractedDir, err := os.Open(tempDir) + return nil +} + +// get the root dir from the extractedPath +// if multiple entries are found in the extractedPath then treat extractedPath as root +func GetFirstTopLevelDir(extractedPath string) (string, error) { + entries, err := os.ReadDir(extractedPath) + fmt.Println("entries %v", entries) if err != nil { - return nil, fmt.Errorf("failed to open extracted directory: %v", err) + return "", fmt.Errorf("failed to read extracted directory: %v", err) } - return extractedDir, nil + if len(entries) == 1 && entries[0].IsDir() { + return filepath.Join(extractedPath, entries[0].Name()), nil + } + return extractedPath, nil } func SanitizeBundle(data []byte, fileName string, ext string, tempDir string) (SanitizedFile, error) { // fmt.Println("temp", tempDir) - extracted, err := ExtractTar(bytes.NewReader(data), fileName, tempDir) + outputDir := path.Join(tempDir, fileName) + + // outputDir, err := os.MkdirTemp(tempDir, fileName) + var err error + + // if err != nil { + // return SanitizedFile{}, fmt.Errorf("Failed to create extraction directory %w", err) + // } + + switch ext { + + case ".tar", ".tar.gz", ".tgz", ".gz": + err = ExtractTar(bytes.NewReader(data), fileName, tempDir) + case ".zip": + target := path.Join(tempDir, fileName) + err = ExtractZipFromBytes(data, target) + default: + return SanitizedFile{}, fmt.Errorf("Unsupported compression extension %s", ext) + } + + if err != nil { + return SanitizedFile{}, err + } + + // jump directly into the extracted dirs toplevel dir (which is the case if a single folder is archived the extracted dir preserves that structure) + + extractedPath, err := GetFirstTopLevelDir(outputDir) + fmt.Println("extracted path %s %s", outputDir, extractedPath) return SanitizedFile{ - FileExt: ext, - ExtractedContent: extracted, - RawData: data, + FileExt: ext, + ExtractedContentPath: extractedPath, + RawData: data, }, err + } func IsValidYaml(data []byte) error { diff --git a/files/tests/samples/valid-docker-compose.yml b/files/tests/samples/valid-docker-compose.yml new file mode 100644 index 00000000..f112c891 --- /dev/null +++ b/files/tests/samples/valid-docker-compose.yml @@ -0,0 +1,37 @@ +version: "3.8" # Docker Compose version + +services: + # NGINX web server + web: + image: nginx:latest # Use the latest NGINX image + container_name: my_web_server # Custom container name + ports: + - "80:80" # Map host port 80 to container port 80 + volumes: + - ./html:/usr/share/nginx/html # Mount a local directory to the container + networks: + - my_network # Attach to a custom network + depends_on: + - db # Ensure the database service starts first + + # PostgreSQL database + db: + image: postgres:13 # Use PostgreSQL version 13 + container_name: my_database # Custom container name + environment: + POSTGRES_USER: admin # Database username + POSTGRES_PASSWORD: secret # Database password + POSTGRES_DB: myapp_db # Database name + volumes: + - postgres_data:/var/lib/postgresql/data # Persist database data + networks: + - my_network # Attach to the same custom network + +# Define custom networks +networks: + my_network: + driver: bridge # Use the default bridge driver + +# Define volumes for persistent data +volumes: + postgres_data: # Volume for PostgreSQL data diff --git a/files/tests/samples/wordpress-kustomize.zip b/files/tests/samples/wordpress-kustomize.zip new file mode 100644 index 0000000000000000000000000000000000000000..43f4771bfc4a5a51d3bef5cb712e1f1643c1f57a GIT binary patch literal 4514 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 Date: Fri, 31 Jan 2025 17:03:55 +0530 Subject: [PATCH 17/33] fix temp dirs while extracting Signed-off-by: aabidsofi19 --- files/identification.go | 2 -- files/sanitization.go | 19 ++++++------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/files/identification.go b/files/identification.go index 8dd1c07a..8912c022 100644 --- a/files/identification.go +++ b/files/identification.go @@ -236,7 +236,6 @@ func ParseFileAsKustomization(file SanitizedFile) (resmap.ResMap, error) { // Ensure the path points to the directory containing kustomization.yaml kustomizationPath := filepath.Join(path, "kustomization.yaml") - fmt.Println("Kustomization Path %s", kustomizationPath) if _, err := os.Stat(kustomizationPath); os.IsNotExist(err) { return nil, fmt.Errorf("kustomization.yaml not found in extracted directory") } @@ -254,7 +253,6 @@ func ParseFileAsKustomization(file SanitizedFile) (resmap.ResMap, error) { return nil, fmt.Errorf("failed to write raw data to in-memory filesystem: %v", err) } } - fmt.Println("kustomization extraxting from path %s", path) // Use krusty to build the Kustomize resources k := krusty.MakeKustomizer(krusty.MakeDefaultOptions()) diff --git a/files/sanitization.go b/files/sanitization.go index be867d25..9ea8c436 100644 --- a/files/sanitization.go +++ b/files/sanitization.go @@ -6,7 +6,6 @@ import ( "bytes" "compress/gzip" "encoding/json" - "path" "fmt" "io" @@ -197,7 +196,6 @@ func ExtractZipFromBytes(data []byte, outputDir string) error { // if multiple entries are found in the extractedPath then treat extractedPath as root func GetFirstTopLevelDir(extractedPath string) (string, error) { entries, err := os.ReadDir(extractedPath) - fmt.Println("entries %v", entries) if err != nil { return "", fmt.Errorf("failed to read extracted directory: %v", err) } @@ -209,23 +207,19 @@ func GetFirstTopLevelDir(extractedPath string) (string, error) { } func SanitizeBundle(data []byte, fileName string, ext string, tempDir string) (SanitizedFile, error) { - // fmt.Println("temp", tempDir) - outputDir := path.Join(tempDir, fileName) - // outputDir, err := os.MkdirTemp(tempDir, fileName) - var err error + outputDir, err := os.MkdirTemp(tempDir, fileName) - // if err != nil { - // return SanitizedFile{}, fmt.Errorf("Failed to create extraction directory %w", err) - // } + if err != nil { + return SanitizedFile{}, fmt.Errorf("Failed to create extraction directory %w", err) + } switch ext { case ".tar", ".tar.gz", ".tgz", ".gz": - err = ExtractTar(bytes.NewReader(data), fileName, tempDir) + err = ExtractTar(bytes.NewReader(data), fileName, outputDir) case ".zip": - target := path.Join(tempDir, fileName) - err = ExtractZipFromBytes(data, target) + err = ExtractZipFromBytes(data, outputDir) default: return SanitizedFile{}, fmt.Errorf("Unsupported compression extension %s", ext) } @@ -237,7 +231,6 @@ func SanitizeBundle(data []byte, fileName string, ext string, tempDir string) (S // jump directly into the extracted dirs toplevel dir (which is the case if a single folder is archived the extracted dir preserves that structure) extractedPath, err := GetFirstTopLevelDir(outputDir) - fmt.Println("extracted path %s %s", outputDir, extractedPath) return SanitizedFile{ FileExt: ext, From ed8713d33a5997e1ea4f584aaf3ae6980cd4b47b Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Fri, 31 Jan 2025 18:06:06 +0530 Subject: [PATCH 18/33] add meshkit errors Signed-off-by: aabidsofi19 --- files/errors.go | 123 +++++++++++++++++++++++++++++++ files/sanitization.go | 34 ++++----- files/tests/sanitization_test.go | 28 ++++--- 3 files changed, 156 insertions(+), 29 deletions(-) create mode 100644 files/errors.go diff --git a/files/errors.go b/files/errors.go new file mode 100644 index 00000000..e0c387af --- /dev/null +++ b/files/errors.go @@ -0,0 +1,123 @@ +package files + +import ( + "fmt" + "maps" + "slices" + "strings" + + "github.com/layer5io/meshkit/errors" +) + +var ( + // Error code + ErrUnsupportedExtensionCode = "replace_me" + ErrFailedToIdentifyFileCode = "replace_me" + ErrSanitizingFileCode = "replace_me" + ErrInvalidYamlCode = "replace_me" + ErrInvalidJsonCode = "replace_me" + ErrFailedToExtractTarCode = "replace_me" +) + +func ErrUnsupportedExtension(fileName string, fileExt string, supportedExtensionsMap map[string]bool) error { + supportedExtensions := slices.Collect(maps.Keys(supportedExtensionsMap)) + + sdescription := []string{ + fmt.Sprintf("The file '%s' has an unsupported extension: '%s'.", fileName, fileExt), + fmt.Sprintf("Supported file extensions are: %s.", strings.Join(supportedExtensions, ", ")), + } + + ldescription := []string{ + fmt.Sprintf("The file '%s' could not be processed because the extension '%s' is not supported by the system.", fileName, fileExt), + fmt.Sprintf("The system is designed to handle files with the following extensions: %s.", strings.Join(supportedExtensions, ", ")), + } + + probableCause := []string{ + "The file extension does not match any of the supported formats.", + "The file may have been saved with an incorrect or unsupported extension.", + } + + remedy := []string{ + "Ensure the file is saved with one of the supported extensions.", + "Convert the file to a supported format before attempting to process it.", + } + + return errors.New(ErrSanitizingFileCode, errors.Critical, sdescription, ldescription, probableCause, remedy) +} + +func ErrInvalidYaml(fileName string, err error) error { + sdescription := []string{ + fmt.Sprintf("The file '%s' contains invalid YAML syntax.", fileName), + } + + ldescription := []string{ + fmt.Sprintf("The file '%s' could not be parsed due to invalid YAML syntax.", fileName), + fmt.Sprintf("Error details: %s", err.Error()), + } + + probableCause := []string{ + "The YAML file may contain syntax errors, such as incorrect indentation, missing colons, or invalid characters.", + "The file may have been corrupted or improperly edited.", + } + + remedy := []string{ + "Review the YAML syntax in the file and correct any errors.", + "Use a YAML validator or linter to identify and fix issues.", + "Ensure the file adheres to the YAML specification.", + } + + return errors.New(ErrInvalidYamlCode, errors.Critical, sdescription, ldescription, probableCause, remedy) +} + +func ErrInvalidJson(fileName string, err error) error { + sdescription := []string{ + fmt.Sprintf("The file '%s' contains invalid JSON syntax.", fileName), + } + + ldescription := []string{ + fmt.Sprintf("The file '%s' could not be parsed due to invalid JSON syntax.", fileName), + fmt.Sprintf("Error details: %s", err.Error()), + } + + probableCause := []string{ + "The JSON file may contain syntax errors, such as missing commas, curly braces, or square brackets.", + "The file may have been corrupted or improperly edited.", + "Special characters or escape sequences may not have been properly formatted.", + } + + remedy := []string{ + "Review the JSON syntax in the file and correct any errors.", + "Use a JSON validator or linter to identify and fix issues.", + "Ensure the file adheres to the JSON specification (e.g., double quotes for keys and strings).", + "Check for common issues like trailing commas or unescaped special characters.", + } + + return errors.New(ErrInvalidJsonCode, errors.Critical, sdescription, ldescription, probableCause, remedy) +} + +func ErrFailedToExtractArchive(fileName string, err error) error { + sdescription := []string{ + fmt.Sprintf("Failed to extract the archive '%s'.", fileName), + } + + ldescription := []string{ + fmt.Sprintf("An error occurred while attempting to extract the TAR archive '%s'.", fileName), + fmt.Sprintf("Error details: %s", err.Error()), + } + + probableCause := []string{ + "The archive may be corrupted or incomplete.", + "The archive may contain unsupported compression formats or features.", + "Insufficient permissions to read or write files during extraction.", + "The archive may have been created with a different tool or version that is incompatible.", + } + + remedy := []string{ + "Verify that the archive is not corrupted by checking its integrity or re-downloading it.", + "Ensure the archive uses a supported compression format (e.g., .zip, .tar, .tar.gz, ).", + "Check that you have sufficient permissions to read the archive and write to the destination directory.", + "Try using a different extraction tool or library to rule out compatibility issues.", + } + + return errors.New(ErrFailedToExtractTarCode, errors.Critical, sdescription, ldescription, probableCause, remedy) +} diff --git a/files/sanitization.go b/files/sanitization.go index 9ea8c436..9f9b871d 100644 --- a/files/sanitization.go +++ b/files/sanitization.go @@ -24,23 +24,13 @@ type SanitizedFile struct { ExtractedContentPath string } -func SanitizeFile(data []byte, fileName string, tempDir string) (SanitizedFile, error) { - - validExts := map[string]bool{ - ".json": true, - ".yml": true, - ".yaml": true, - ".tar": true, - ".tar.gz": true, - ".tgz": true, - ".zip": true, - } +func SanitizeFile(data []byte, fileName string, tempDir string, validExts map[string]bool) (SanitizedFile, error) { ext := filepath.Ext(fileName) // 1. Check if file has supported extension if !validExts[ext] && !validExts[filepath.Ext(strings.TrimSuffix(fileName, ".gz"))] { - return SanitizedFile{}, fmt.Errorf("unsupported file extension: %s", ext) + return SanitizedFile{}, ErrUnsupportedExtension(fileName, ext, validExts) } switch ext { @@ -48,7 +38,7 @@ func SanitizeFile(data []byte, fileName string, tempDir string) (SanitizedFile, case ".yml", ".yaml": err := IsValidYaml(data) if err != nil { - return SanitizedFile{}, fmt.Errorf("File is not valid yaml: %w", err) + return SanitizedFile{}, ErrInvalidYaml(fileName, err) } return SanitizedFile{ @@ -60,7 +50,7 @@ func SanitizeFile(data []byte, fileName string, tempDir string) (SanitizedFile, err := IsValidJson(data) if err != nil { - return SanitizedFile{}, fmt.Errorf("File is not valid json: %w", err) + return SanitizedFile{}, ErrInvalidJson(fileName, err) } return SanitizedFile{ @@ -74,7 +64,7 @@ func SanitizeFile(data []byte, fileName string, tempDir string) (SanitizedFile, } - return SanitizedFile{}, fmt.Errorf("Unsupported file extension %s", ext) + return SanitizedFile{}, ErrUnsupportedExtension(fileName, ext, validExts) } @@ -86,7 +76,7 @@ func ExtractTar(reader io.Reader, archiveFile string, outputDir string) error { if strings.HasSuffix(archiveFile, ".gz") || strings.HasSuffix(archiveFile, ".tgz") { gzipReader, err := gzip.NewReader(reader) if err != nil { - return fmt.Errorf("failed to create gzip reader: %v", err) + return err } defer gzipReader.Close() reader = gzipReader @@ -103,7 +93,7 @@ func ExtractTar(reader io.Reader, archiveFile string, outputDir string) error { break // End of archive } if err != nil { - return fmt.Errorf("failed to read tar archive: %v", err) + return err } // Construct the full path for the file/directory @@ -211,7 +201,7 @@ func SanitizeBundle(data []byte, fileName string, ext string, tempDir string) (S outputDir, err := os.MkdirTemp(tempDir, fileName) if err != nil { - return SanitizedFile{}, fmt.Errorf("Failed to create extraction directory %w", err) + return SanitizedFile{}, ErrFailedToExtractArchive(fileName, fmt.Errorf("Failed to create extraction directory %w", err)) } switch ext { @@ -221,7 +211,7 @@ func SanitizeBundle(data []byte, fileName string, ext string, tempDir string) (S case ".zip": err = ExtractZipFromBytes(data, outputDir) default: - return SanitizedFile{}, fmt.Errorf("Unsupported compression extension %s", ext) + return SanitizedFile{}, ErrFailedToExtractArchive(fileName, fmt.Errorf("Unsupported compression extension %s", ext)) } if err != nil { @@ -232,11 +222,15 @@ func SanitizeBundle(data []byte, fileName string, ext string, tempDir string) (S extractedPath, err := GetFirstTopLevelDir(outputDir) + if err != nil { + return SanitizedFile{}, ErrFailedToExtractArchive(fileName, err) + } + return SanitizedFile{ FileExt: ext, ExtractedContentPath: extractedPath, RawData: data, - }, err + }, nil } diff --git a/files/tests/sanitization_test.go b/files/tests/sanitization_test.go index 49f065c8..5d0a4275 100644 --- a/files/tests/sanitization_test.go +++ b/files/tests/sanitization_test.go @@ -1,11 +1,11 @@ package files_test import ( - "fmt" "os" "path/filepath" "testing" + "github.com/layer5io/meshkit/errors" "github.com/layer5io/meshkit/files" ) @@ -15,7 +15,7 @@ func TestSanitizeFile(t *testing.T) { filePath string expectedExt string expectError bool - expectedErrMsg string + expectedErrCode string expectedContent map[string]interface{} expectedType string }{ @@ -40,10 +40,10 @@ func TestSanitizeFile(t *testing.T) { expectError: true, }, { - name: "Unsupported extension", - filePath: "./samples/valid.txt", - expectError: true, - expectedErrMsg: fmt.Sprintf("unsupported file extension: %s", ".txt"), + name: "Unsupported extension", + filePath: "./samples/valid.txt", + expectError: true, + expectedErrCode: files.ErrUnsupportedExtensionCode, }, { name: "Valid compressed extension", @@ -105,6 +105,16 @@ func TestSanitizeFile(t *testing.T) { }, } + validExts := map[string]bool{ + ".json": true, + ".yml": true, + ".yaml": true, + ".tar": true, + ".tar.gz": true, + ".tgz": true, + ".zip": true, + } + tempDir, _ := os.MkdirTemp(" ", "temp-tests") defer os.RemoveAll(tempDir) // tempDir := "./temp" @@ -120,13 +130,13 @@ func TestSanitizeFile(t *testing.T) { } t.Run(tc.name, func(t *testing.T) { - result, err := files.SanitizeFile(data, filename, tempDir) + result, err := files.SanitizeFile(data, filename, tempDir, validExts) if tc.expectError { if err == nil { t.Error("Expected error, got nil") - } else if tc.expectedErrMsg != "" && err.Error() != tc.expectedErrMsg { - t.Errorf("Expected error message %q, got %q", tc.expectedErrMsg, err.Error()) + } else if tc.expectedErrCode != "" && errors.GetCode(err) != tc.expectedErrCode { + t.Errorf("Expected error message %q, got %q", tc.expectedErrCode, err.Error()) } return } From 7accbaf151df7326b38b023e45e2bf3ddd7af5f6 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Fri, 31 Jan 2025 18:40:30 +0530 Subject: [PATCH 19/33] add identification trace and more errors Signed-off-by: aabidsofi19 --- files/errors.go | 181 ++++++++++++++++++++++++++++++++++++++-- files/identification.go | 17 +++- files/sanitization.go | 16 ++-- 3 files changed, 200 insertions(+), 14 deletions(-) diff --git a/files/errors.go b/files/errors.go index e0c387af..b9791e3c 100644 --- a/files/errors.go +++ b/files/errors.go @@ -11,14 +11,47 @@ import ( var ( // Error code - ErrUnsupportedExtensionCode = "replace_me" - ErrFailedToIdentifyFileCode = "replace_me" - ErrSanitizingFileCode = "replace_me" - ErrInvalidYamlCode = "replace_me" - ErrInvalidJsonCode = "replace_me" - ErrFailedToExtractTarCode = "replace_me" + ErrUnsupportedExtensionCode = "replace_me" + ErrUnsupportedExtensionForOperationCode = "replace_me" + ErrFailedToIdentifyFileCode = "replace_me" + ErrSanitizingFileCode = "replace_me" + ErrInvalidYamlCode = "replace_me" + ErrInvalidJsonCode = "replace_me" + ErrFailedToExtractTarCode = "replace_me" + ErrUnsupportedFileTypeCode = "replace_me" + ErrInvalidKubernetesManifestCode = "replace_me" + ErrInvalidMesheryDesignCode = "replace_me" + ErrInvalidHelmChartCode = "replace_me" + ErrInvalidDockerComposeCode = "replace_me" + ErrInvalidKustomizationCode = "replace_me" ) +func ErrUnsupportedExtensionForOperation(operation string, fileName string, fileExt string, supportedExtensions []string) error { + sdescription := []string{ + fmt.Sprintf("The file '%s' has an unsupported extension '%s' for the operation '%s'.", fileName, fileExt, operation), + fmt.Sprintf("Supported extensions for this operation are: %s.", strings.Join(supportedExtensions, ", ")), + } + + ldescription := []string{ + fmt.Sprintf("The file '%s' could not be used for the operation '%s' because the extension '%s' is not supported.", fileName, operation, fileExt), + fmt.Sprintf("The system is designed to handle files with the following extensions for this operation: %s.", strings.Join(supportedExtensions, ", ")), + } + + probableCause := []string{ + "The file extension does not match any of the supported formats for this operation.", + "The file may have been saved with an incorrect or unsupported extension.", + "The operation may have specific requirements for file types that are not met by this file.", + } + + remedy := []string{ + "Ensure the file is saved with one of the supported extensions for this operation.", + "Convert the file to a supported format before attempting the operation.", + "Check the documentation or requirements for the operation to verify the supported file types.", + } + + return errors.New(ErrUnsupportedExtensionForOperationCode, errors.Critical, sdescription, ldescription, probableCause, remedy) +} + func ErrUnsupportedExtension(fileName string, fileExt string, supportedExtensionsMap map[string]bool) error { supportedExtensions := slices.Collect(maps.Keys(supportedExtensionsMap)) @@ -121,3 +154,139 @@ func ErrFailedToExtractArchive(fileName string, err error) error { return errors.New(ErrFailedToExtractTarCode, errors.Critical, sdescription, ldescription, probableCause, remedy) } + +func ErrFailedToIdentifyFile(fileName string, fileExt string, identificationTrace map[string]error) error { + + validTypes := slices.Collect(maps.Keys(identificationTrace)) + + sdescription := []string{ + "The file '%s' could not be identified as a supported type", + } + + // Build a detailed trace of identification attempts + var traceDetails []string + for fileType, err := range identificationTrace { + traceDetails = append(traceDetails, fmt.Sprintf("- Attempted to identify as '%s': %v", fileType, err)) + } + + ldescription := []string{ + fmt.Sprintf("The file '%s' was not recognized as any of the supported file types %v.", fileName, validTypes), + fmt.Sprintf("Identification attempts and errors:"), + } + ldescription = append(ldescription, traceDetails...) + + probableCause := []string{ + "The file extension does not match any of the supported types.", + "The file may be corrupted or incomplete.", + "The file may have been saved with an incorrect or unsupported format.", + "The file may not conform to the expected structure for any supported type.", + } + + remedy := []string{ + "Ensure the file is saved with one of the supported extensions.", + "Verify the integrity of the file and ensure it is not corrupted.", + "Check the file's content and structure to ensure it matches one of the supported types.", + "Convert the file to a supported format before attempting to process it.", + } + return errors.New(ErrUnsupportedFileTypeCode, errors.Critical, sdescription, ldescription, probableCause, remedy) +} + +func ErrInvalidKubernetesManifest(fileName string, err error) error { + sdescription := []string{ + fmt.Sprintf("The file '%s' is not a valid Kubernetes manifest.", fileName), + } + + ldescription := []string{ + fmt.Sprintf("The file '%s' could not be parsed as a Kubernetes manifest due to invalid syntax or structure.", fileName), + fmt.Sprintf("Error details: %s", err.Error()), + } + + probableCause := []string{ + "The file may contain invalid YAML syntax or incorrect indentation.", + "The file may not conform to the Kubernetes API schema.", + "The file may be missing required fields or contain unsupported fields.", + } + + remedy := []string{ + "Review the YAML syntax in the file and correct any errors.", + "Use a Kubernetes YAML validator or linter to identify and fix issues.", + "Ensure the file adheres to the Kubernetes API specification.", + } + + return errors.New(ErrInvalidKubernetesManifestCode, errors.Critical, sdescription, ldescription, probableCause, remedy) +} + +func ErrInvalidHelmChart(fileName string, err error) error { + sdescription := []string{ + fmt.Sprintf("The file '%s' is not a valid Helm chart.", fileName), + } + + ldescription := []string{ + fmt.Sprintf("The file '%s' could not be parsed as a Helm chart due to invalid structure or missing files.", fileName), + fmt.Sprintf("Error details: %s", err.Error()), + } + + probableCause := []string{ + "The file may be missing required files such as 'Chart.yaml' or 'values.yaml'.", + "The file may be corrupted or incomplete.", + "The file may not conform to the Helm chart specification.", + } + + remedy := []string{ + "Ensure the file contains all required Helm chart files (e.g., Chart.yaml, values.yaml).", + "Verify the integrity of the Helm chart archive.", + "Check the Helm documentation for the correct chart structure.", + } + + return errors.New(ErrInvalidHelmChartCode, errors.Critical, sdescription, ldescription, probableCause, remedy) +} + +func ErrInvalidDockerCompose(fileName string, err error) error { + sdescription := []string{ + fmt.Sprintf("The file '%s' is not a valid Docker Compose file.", fileName), + } + + ldescription := []string{ + fmt.Sprintf("The file '%s' could not be parsed as a Docker Compose file due to invalid syntax or structure.", fileName), + fmt.Sprintf("Error details: %s", err.Error()), + } + + probableCause := []string{ + "The file may contain invalid YAML syntax or incorrect indentation.", + "The file may not conform to the Docker Compose specification.", + "The file may be missing required fields or contain unsupported fields.", + } + + remedy := []string{ + "Review the YAML syntax in the file and correct any errors.", + "Use a Docker Compose validator or linter to identify and fix issues.", + "Ensure the file adheres to the Docker Compose specification.", + } + + return errors.New(ErrInvalidDockerComposeCode, errors.Critical, sdescription, ldescription, probableCause, remedy) +} + +func ErrInvalidKustomization(fileName string, err error) error { + sdescription := []string{ + fmt.Sprintf("The file '%s' is not a valid Kustomization file.", fileName), + } + + ldescription := []string{ + fmt.Sprintf("The file '%s' could not be parsed as a Kustomization file due to invalid syntax or structure.", fileName), + fmt.Sprintf("Error details: %s", err.Error()), + } + + probableCause := []string{ + "The file may be missing required fields such as 'resources' or 'bases'.", + "The file may contain invalid YAML syntax or incorrect indentation.", + "The file may not conform to the Kustomize specification.", + } + + remedy := []string{ + "Review the YAML syntax in the file and correct any errors.", + "Ensure the file contains all required fields for Kustomization.", + "Check the Kustomize documentation for the correct file structure.", + } + + return errors.New(ErrInvalidKustomizationCode, errors.Critical, sdescription, ldescription, probableCause, remedy) +} diff --git a/files/identification.go b/files/identification.go index 8912c022..9e026269 100644 --- a/files/identification.go +++ b/files/identification.go @@ -50,44 +50,57 @@ func IdentifyFile(sanitizedFile SanitizedFile) (IdentifiedFile, error) { var err error var parsed interface{} + // Map to store identification errors for each file type + identificationErrorsTrace := map[string]error{} + + // Attempt to parse the file as a Meshery design if parsed, err = ParseFileAsMesheryDesign(sanitizedFile); err == nil { return IdentifiedFile{ Type: MESHERY_DESIGN, ParsedFile: parsed, }, nil } + identificationErrorsTrace[MESHERY_DESIGN] = err + // Attempt to parse the file as a Kubernetes manifest if parsed, err = ParseFileAsKubernetesManifest(sanitizedFile); err == nil { return IdentifiedFile{ Type: KUBERNETES_MANIFEST, ParsedFile: parsed, }, nil } + identificationErrorsTrace[KUBERNETES_MANIFEST] = err + // Attempt to parse the file as a Helm chart if parsed, err = ParseFileAsHelmChart(sanitizedFile); err == nil { return IdentifiedFile{ Type: HELM_CHART, ParsedFile: parsed, }, nil } + identificationErrorsTrace[HELM_CHART] = err + // Attempt to parse the file as a Docker Compose file if parsed, err = ParseFileAsDockerCompose(sanitizedFile); err == nil { return IdentifiedFile{ Type: DOCKER_COMPOSE, ParsedFile: parsed, }, nil } + identificationErrorsTrace[DOCKER_COMPOSE] = err + // Attempt to parse the file as a Kustomization file if parsed, err = ParseFileAsKustomization(sanitizedFile); err == nil { return IdentifiedFile{ Type: KUSTOMIZATION, ParsedFile: parsed, }, nil } + identificationErrorsTrace[KUSTOMIZATION] = err - return IdentifiedFile{}, fmt.Errorf("Unsupported FileType %w", err) + // If no file type matched, return a detailed error with the identification trace + return IdentifiedFile{}, ErrFailedToIdentifyFile(sanitizedFile.FileName, sanitizedFile.FileExt, identificationErrorsTrace) } - func ParseFileAsMesheryDesign(file SanitizedFile) (pattern.PatternFile, error) { var parsed pattern.PatternFile diff --git a/files/sanitization.go b/files/sanitization.go index 9f9b871d..e6a11175 100644 --- a/files/sanitization.go +++ b/files/sanitization.go @@ -18,8 +18,9 @@ import ( ) type SanitizedFile struct { - FileExt string - RawData []byte + FileExt string + FileName string + RawData []byte // incase of bundle like tar ExtractedContentPath string } @@ -42,8 +43,9 @@ func SanitizeFile(data []byte, fileName string, tempDir string, validExts map[st } return SanitizedFile{ - FileExt: ext, - RawData: data, + FileExt: ext, + FileName: fileName, + RawData: data, }, nil case ".json": @@ -54,8 +56,9 @@ func SanitizeFile(data []byte, fileName string, tempDir string, validExts map[st } return SanitizedFile{ - FileExt: ext, - RawData: data, + FileExt: ext, + RawData: data, + FileName: fileName, }, nil case ".tar", ".tar.gz", ".zip", ".gz", ".tgz": @@ -228,6 +231,7 @@ func SanitizeBundle(data []byte, fileName string, ext string, tempDir string) (S return SanitizedFile{ FileExt: ext, + FileName: fileName, ExtractedContentPath: extractedPath, RawData: data, }, nil From 59418e7e1d6c1295a64980f53b3fb71200af3707 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Fri, 31 Jan 2025 23:28:56 +0530 Subject: [PATCH 20/33] more test scenarios Signed-off-by: aabidsofi19 --- files/identification.go | 2 -- files/tests/sanitization_test.go | 14 ++++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/files/identification.go b/files/identification.go index 9e026269..e0c0073c 100644 --- a/files/identification.go +++ b/files/identification.go @@ -18,8 +18,6 @@ import ( "sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/kyaml/filesys" - // appsv1 "k8s.io/api/apps/v1" - // corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/kubernetes/scheme" diff --git a/files/tests/sanitization_test.go b/files/tests/sanitization_test.go index 5d0a4275..6b6de9fe 100644 --- a/files/tests/sanitization_test.go +++ b/files/tests/sanitization_test.go @@ -20,9 +20,10 @@ func TestSanitizeFile(t *testing.T) { expectedType string }{ { - name: "Valid JSON", - filePath: "./samples/valid.json", - expectedExt: ".json", + name: "Valid JSON", + filePath: "./samples/valid.json", + expectedExt: ".json", + expectedType: "", }, { name: "Invalid JSON", @@ -35,9 +36,10 @@ func TestSanitizeFile(t *testing.T) { expectedExt: ".yml", }, { - name: "Invalid YAML", - filePath: "./samples/invalid.yml", - expectError: true, + name: "Invalid YAML", + filePath: "./samples/invalid.yml", + expectError: true, + expectedType: "", }, { name: "Unsupported extension", From 761003a3fe2e5bda8d4b275beedd17b9dbb39b78 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Fri, 31 Jan 2025 23:35:25 +0530 Subject: [PATCH 21/33] go mod tidy Signed-off-by: aabidsofi19 --- go.mod | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 53af3d4c..dcabd374 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ replace ( require ( cuelang.org/go v0.11.2 github.com/Masterminds/semver/v3 v3.3.1 + github.com/docker/cli v27.5.1+incompatible github.com/fluxcd/pkg/oci v0.43.1 github.com/fluxcd/pkg/tar v0.10.0 github.com/go-git/go-git/v5 v5.13.2 @@ -45,6 +46,8 @@ require ( k8s.io/client-go v0.32.1 k8s.io/kubectl v0.32.1 oras.land/oras-go/v2 v2.5.0 + sigs.k8s.io/kustomize/api v0.19.0 + sigs.k8s.io/kustomize/kyaml v0.19.0 ) require ( @@ -98,7 +101,6 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deckarep/golang-set v1.8.0 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/cli v27.5.1+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v27.5.1+incompatible // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect @@ -266,8 +268,6 @@ require ( oras.land/oras-go v1.2.6 // indirect sigs.k8s.io/controller-runtime v0.20.1 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect - sigs.k8s.io/kustomize/api v0.19.0 // indirect - sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) From 60e44ee27657648c3eeb298c196e26cf959354e7 Mon Sep 17 00:00:00 2001 From: aabidsofi19 <65964225+aabidsofi19@users.noreply.github.com> Date: Sat, 1 Feb 2025 12:55:56 +0530 Subject: [PATCH 22/33] Update files/errors.go Co-authored-by: Shlok Mishra <99207534+Jougan-0@users.noreply.github.com> Signed-off-by: aabidsofi19 <65964225+aabidsofi19@users.noreply.github.com> --- files/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/errors.go b/files/errors.go index b9791e3c..d051bd51 100644 --- a/files/errors.go +++ b/files/errors.go @@ -171,7 +171,7 @@ func ErrFailedToIdentifyFile(fileName string, fileExt string, identificationTrac ldescription := []string{ fmt.Sprintf("The file '%s' was not recognized as any of the supported file types %v.", fileName, validTypes), - fmt.Sprintf("Identification attempts and errors:"), + "Identification attempts and errors:", } ldescription = append(ldescription, traceDetails...) From d993d69f47b7c2438ecf9c92213abb2b78b139d2 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Sat, 1 Feb 2025 18:19:43 +0530 Subject: [PATCH 23/33] fallback to unstructured parsing for unknown schemes eg crds Signed-off-by: aabidsofi19 --- files/identification.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/files/identification.go b/files/identification.go index e0c0073c..28decc7c 100644 --- a/files/identification.go +++ b/files/identification.go @@ -13,6 +13,7 @@ import ( "gopkg.in/yaml.v3" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" k8sYaml "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/kustomize/api/krusty" "sigs.k8s.io/kustomize/api/resmap" @@ -164,7 +165,12 @@ func ParseFileAsKubernetesManifest(file SanitizedFile) ([]runtime.Object, error) // Decode the raw YAML into a runtime.Object obj, _, err := decode(raw.Raw, nil, nil) if err != nil { - return nil, fmt.Errorf("failed to decode YAML into Kubernetes object: %v", err) + // Fallback: Convert to Unstructured object for unknown API types + unstructuredObj := &unstructured.Unstructured{} + if err := json.Unmarshal(raw.Raw, unstructuredObj); err != nil { + return nil, fmt.Errorf("failed to decode YAML into Kubernetes object: %v", err) + } + objects = append(objects, unstructuredObj) } objects = append(objects, obj) From 9f136809e2f79fe83506b62a916b62dac8e75928 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Sat, 1 Feb 2025 18:19:58 +0530 Subject: [PATCH 24/33] add more tests Signed-off-by: aabidsofi19 --- files/tests/samples/manifest-with-crds.yml | 417 +++++++++++++++++++++ files/tests/sanitization_test.go | 7 + 2 files changed, 424 insertions(+) create mode 100644 files/tests/samples/manifest-with-crds.yml diff --git a/files/tests/samples/manifest-with-crds.yml b/files/tests/samples/manifest-with-crds.yml new file mode 100644 index 00000000..700d9221 --- /dev/null +++ b/files/tests/samples/manifest-with-crds.yml @@ -0,0 +1,417 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: {} + name: redis-follower + namespace: default +spec: + replicas: 2 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + role: follower + tier: backend + spec: + containers: + - ports: + - containerPort: 6379 + - image: us-docker.pkg.dev/google-samples/containers/gke/gb-redis-follower:v2 + name: follower + ports: + - containerPort: 6379 + resources: + requests: + cpu: 100m + memory: 100Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: {} + name: redis-leader + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + role: leader + tier: backend + spec: + containers: + - ports: + - containerPort: 6379 + - image: docker.io/redis:6.0.5 + name: leader + ports: + - containerPort: 6379 + resources: + requests: + cpu: 100m + memory: 100Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: {} + name: configmap-volume + namespace: default +spec: + replicas: 3 + selector: + matchLabels: + app.kubernetes.io/name: configmap-volume + template: + metadata: + labels: + app.kubernetes.io/name: configmap-volume + spec: + containers: + - command: + - /bin/sh + - -c + - while true; do echo "$(date) My preferred sport is $(cat /etc/config/sport)"; sleep 10; done; + image: alpine:3 + name: alpine + ports: + - containerPort: 80 + volumeMounts: + - mountPath: /etc/config + name: config-volume + volumes: + - configMap: + name: sport + name: config-volume +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: {} + name: frontend + namespace: default +spec: + replicas: 3 + selector: + matchLabels: + app: guestbook + tier: frontend + template: + metadata: + labels: + app: guestbook + tier: frontend + spec: + containers: + - env: + - name: GET_HOSTS_FROM + value: dns + image: us-docker.pkg.dev/google-samples/containers/gke/gb-frontend:v5 + name: php-redis + ports: + - containerPort: 80 + resources: + requests: + cpu: 100m + memory: 100Mi +--- +apiVersion: source.toolkit.fluxcd.io/v1beta1 +kind: GitRepository +metadata: + annotations: {} + labels: {} + name: git-repository-ie + namespace: default +spec: + gitImplementation: go-git + ref: + branch: master + timeout: 20s +--- +apiVersion: garo.tietoevry.com/v1alpha1 +kind: GithubActionRunner +metadata: + annotations: {} + labels: {} + name: github-action-runner-ds + namespace: default +spec: + deletionOrder: Least Recent + minRunners: 1 + minTtl: 0m + podTemplateSpec: + metadata: + finalizers: [] + spec: + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: [] + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: [] + podAffinity: + preferredDuringSchedulingIgnoredDuringExecution: [] + requiredDuringSchedulingIgnoredDuringExecution: [] + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: [] + requiredDuringSchedulingIgnoredDuringExecution: [] + containers: [] + dnsConfig: + nameservers: [] + options: [] + searches: [] + ephemeralContainers: [] + hostAliases: [] + imagePullSecrets: [] + initContainers: [] + readinessGates: [] + resourceClaims: [] + schedulingGates: [] + securityContext: + supplementalGroups: [] + sysctls: [] + tolerations: [] + topologySpreadConstraints: [] + volumes: [] + reconciliationPeriod: 1m +--- +apiVersion: v1 +kind: Namespace +metadata: + annotations: {} + labels: {} + name: default + namespace: default +--- +apiVersion: v1 +kind: Namespace +metadata: + annotations: {} + labels: {} + name: default + namespace: default +--- +apiVersion: v1 +kind: Namespace +metadata: + annotations: {} + labels: {} + name: default + namespace: default +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-gjt + namespace: default +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-rny + namespace: default +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-tjt + namespace: default +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-uxe + namespace: default +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-yx + namespace: default +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-spd + namespace: default +spec: + containers: + - command: + - /bin/sh + - -c + - while true; do echo "$(date) My preferred sport is $(cat /etc/config/sport)"; sleep 10; done; + image: alpine:3 + name: alpine + ports: + - containerPort: 80 + volumeMounts: + - mountPath: /etc/config + name: config-volume + volumes: + - configMap: + name: sport + name: config-volume +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-cfl + namespace: default +spec: + containers: + - env: + - name: GET_HOSTS_FROM + value: dns + image: us-docker.pkg.dev/google-samples/containers/gke/gb-frontend:v5 + name: php-redis + ports: + - containerPort: 80 + resources: + requests: + cpu: 100m + memory: 100Mi +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-nip + namespace: default +spec: + containers: + - image: docker.io/redis:6.0.5 + name: leader + ports: + - containerPort: 6379 + resources: + requests: + cpu: 100m + memory: 100Mi +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-znv + namespace: default +spec: + containers: + - image: us-docker.pkg.dev/google-samples/containers/gke/gb-redis-follower:v2 + name: follower + ports: + - containerPort: 6379 + resources: + requests: + cpu: 100m + memory: 100Mi +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-kea + namespace: default +spec: + containers: + - ports: + - containerPort: 6379 + - image: docker.io/redis:6.0.5 + name: leader + ports: + - containerPort: 6379 + resources: + requests: + cpu: 100m + memory: 100Mi +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: {} + labels: {} + name: pod-hgg + namespace: default +spec: + containers: + - ports: + - containerPort: 6379 + - image: us-docker.pkg.dev/google-samples/containers/gke/gb-redis-follower:v2 + name: follower + ports: + - containerPort: 6379 + resources: + requests: + cpu: 100m + memory: 100Mi +--- +apiVersion: v1 +kind: Service +metadata: + annotations: {} + labels: {} + name: frontend + namespace: default +spec: + ports: + - port: 80 + selector: + app: guestbook + tier: frontend +--- +apiVersion: v1 +kind: Service +metadata: + annotations: {} + labels: {} + name: redis-follower + namespace: default +spec: + ports: + - port: 6379 + selector: + app: redis + role: follower + tier: backend +--- +apiVersion: v1 +kind: Service +metadata: + annotations: {} + labels: {} + name: redis-leader + namespace: default +spec: + ports: + - port: 6379 + targetPort: 6379 + selector: + app: redis + role: leader + tier: backend diff --git a/files/tests/sanitization_test.go b/files/tests/sanitization_test.go index 6b6de9fe..3e36f93a 100644 --- a/files/tests/sanitization_test.go +++ b/files/tests/sanitization_test.go @@ -81,6 +81,13 @@ func TestSanitizeFile(t *testing.T) { expectedType: files.KUBERNETES_MANIFEST, }, + { + name: "Can Identify Kubernetes Manifest With Crds", + filePath: "./samples/manifest-with-crds.yml", + expectedExt: ".yml", + expectedType: files.KUBERNETES_MANIFEST, + }, + { name: "Can Identify HelmChart", filePath: "./samples/valid-helm.tgz", From d32f5a3e76f7c7d4400bd5e8d9cf5a49789abd08 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Sat, 1 Feb 2025 18:20:08 +0530 Subject: [PATCH 25/33] add conversion logic Signed-off-by: aabidsofi19 --- files/conversion.go | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 files/conversion.go diff --git a/files/conversion.go b/files/conversion.go new file mode 100644 index 00000000..280af6c0 --- /dev/null +++ b/files/conversion.go @@ -0,0 +1,39 @@ +package files + +import ( + "fmt" + + dockerTypes "github.com/docker/cli/cli/compose/types" + + "github.com/layer5io/meshkit/utils/helm" + "github.com/layer5io/meshkit/utils/kubernetes/kompose" + "gopkg.in/yaml.v3" + "helm.sh/helm/v3/pkg/chart" +) + +func ConvertHelmChartToKubernetesManifest(file IdentifiedFile) (string, error) { + chart, ok := file.ParsedFile.(*chart.Chart) + if chart != nil && !ok { + return "", fmt.Errorf("Failed to get *chart.Chart from identified file") + } + // empty kubernetes version because helm should figure it out + manifest, err := helm.DryRunHelmChart(chart, "") + if err != nil { + return "", err + } + return string(manifest), nil +} + +func ConvertDockerComposeToKubernetesManifest(file IdentifiedFile) (string, error) { + composeConfig, ok := file.ParsedFile.(*dockerTypes.Config) + if composeConfig != nil && !ok { + return "", fmt.Errorf("Failed to get *chart.Chart from identified file") + } + + yamlBytes, err := yaml.Marshal(composeConfig) + + if err != nil { + return "", err + } + return kompose.Convert(yamlBytes) +} From 44a2194664afcad4428c7031e650e195dc4a5454 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Sat, 1 Feb 2025 20:46:52 +0530 Subject: [PATCH 26/33] update docker parsing logic Signed-off-by: aabidsofi19 --- files/conversion.go | 17 ++++----- files/identification.go | 34 ++++++++++++++---- files/tests/samples/valid-compose-2.yml | 14 ++++++++ files/tests/samples/valid-compose-3.yml | 47 +++++++++++++++++++++++++ files/tests/sanitization_test.go | 14 ++++++++ 5 files changed, 109 insertions(+), 17 deletions(-) create mode 100644 files/tests/samples/valid-compose-2.yml create mode 100644 files/tests/samples/valid-compose-3.yml diff --git a/files/conversion.go b/files/conversion.go index 280af6c0..cf8ad393 100644 --- a/files/conversion.go +++ b/files/conversion.go @@ -3,11 +3,11 @@ package files import ( "fmt" - dockerTypes "github.com/docker/cli/cli/compose/types" + // dockerTypes "github.com/docker/cli/cli/compose/types" "github.com/layer5io/meshkit/utils/helm" - "github.com/layer5io/meshkit/utils/kubernetes/kompose" - "gopkg.in/yaml.v3" + // "github.com/layer5io/meshkit/utils/kubernetes/kompose" + // "gopkg.in/yaml.v3" "helm.sh/helm/v3/pkg/chart" ) @@ -25,15 +25,10 @@ func ConvertHelmChartToKubernetesManifest(file IdentifiedFile) (string, error) { } func ConvertDockerComposeToKubernetesManifest(file IdentifiedFile) (string, error) { - composeConfig, ok := file.ParsedFile.(*dockerTypes.Config) - if composeConfig != nil && !ok { + parsedCompose, ok := file.ParsedFile.(ParsedCompose) + if !ok { return "", fmt.Errorf("Failed to get *chart.Chart from identified file") } - yamlBytes, err := yaml.Marshal(composeConfig) - - if err != nil { - return "", err - } - return kompose.Convert(yamlBytes) + return parsedCompose.manifest, nil } diff --git a/files/identification.go b/files/identification.go index 28decc7c..f54f644c 100644 --- a/files/identification.go +++ b/files/identification.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" + "github.com/layer5io/meshkit/utils/kubernetes/kompose" "github.com/meshery/schemas/models/v1beta1/pattern" "gopkg.in/yaml.v3" "helm.sh/helm/v3/pkg/chart" @@ -282,18 +283,19 @@ func ParseFileAsKustomization(file SanitizedFile) (resmap.ResMap, error) { } // ParseFileAsDockerCompose parses a Docker Compose file into a types.Config struct. -func ParseFileAsDockerCompose(file SanitizedFile) (*dockerTypes.Config, error) { - // Parse the YAML file into a map[string]interface{} - var rawConfig map[string]interface{} - if err := yaml.Unmarshal(file.RawData, &rawConfig); err != nil { - return nil, fmt.Errorf("failed to unmarshal YAML: %v", err) +func ParseFileAsDockerComposeStrict(file SanitizedFile) (*dockerTypes.Config, error) { + + // Step 1: Parse YAML using Docker’s built-in loader (ensures correct types) + parsedConfig, err := dockerLoader.ParseYAML(file.RawData) + if err != nil { + return nil, fmt.Errorf("failed to parse Docker Compose YAML: %v", err) } // Use Docker Compose's loader to parse the raw config into a types.Config configDetails := dockerTypes.ConfigDetails{ ConfigFiles: []dockerTypes.ConfigFile{ { - Config: rawConfig, + Config: parsedConfig, }, }, Environment: map[string]string{}, // Optional: Add environment variables if needed @@ -306,3 +308,23 @@ func ParseFileAsDockerCompose(file SanitizedFile) (*dockerTypes.Config, error) { return config, nil } + +type ParsedCompose struct { + manifest string +} + +// ParseFileAsDockerCompose parses a Docker Compose file into a types.Config struct. +func ParseFileAsDockerCompose(file SanitizedFile) (ParsedCompose, error) { + + manifest, err := kompose.Convert(file.RawData) + + // fmt.Println("manifest ", manifest) + + if err != nil { + return ParsedCompose{}, fmt.Errorf("failed to load Docker Compose config: %v", err) + } + + return ParsedCompose{ + manifest: manifest, + }, nil +} diff --git a/files/tests/samples/valid-compose-2.yml b/files/tests/samples/valid-compose-2.yml new file mode 100644 index 00000000..ad0c6340 --- /dev/null +++ b/files/tests/samples/valid-compose-2.yml @@ -0,0 +1,14 @@ +version : "3.8" +services: + minecraft: + image: itzg/minecraft-server + ports: + - "25565:25565" + environment: + EULA: "TRUE" + deploy: + resources: + limits: + memory: 1.5G + volumes: + - "~/minecraft_data:/data" diff --git a/files/tests/samples/valid-compose-3.yml b/files/tests/samples/valid-compose-3.yml new file mode 100644 index 00000000..3d468558 --- /dev/null +++ b/files/tests/samples/valid-compose-3.yml @@ -0,0 +1,47 @@ +version: "3.8" +services: + backend: + build: + context: backend + restart: always + secrets: + - db-password + depends_on: + db: + condition: service_healthy + environment: + - ASPNETCORE_URLS=http://+:8000 + + db: + # We use a mariadb image which supports both amd64 & arm64 architecture + image: mariadb:10-focal + # If you really want to use MySQL, uncomment the following line + #image: mysql:8 + command: '--default-authentication-plugin=mysql_native_password' + restart: always + healthcheck: + test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 --password="$$(cat /run/secrets/db-password)" --silent'] + interval: 3s + retries: 5 + start_period: 30s + secrets: + - db-password + volumes: + - db-data:/var/lib/mysql + environment: + - MYSQL_DATABASE=example + - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db-password + + proxy: + build: proxy + ports: + - 80:80 + depends_on: + - backend + +volumes: + db-data: + +secrets: + db-password: + file: db/password.txt diff --git a/files/tests/sanitization_test.go b/files/tests/sanitization_test.go index 3e36f93a..f204f832 100644 --- a/files/tests/sanitization_test.go +++ b/files/tests/sanitization_test.go @@ -112,6 +112,20 @@ func TestSanitizeFile(t *testing.T) { expectedExt: ".yml", expectedType: files.DOCKER_COMPOSE, }, + + { + name: "Can Identify Docker Compose v2", + filePath: "./samples/valid-compose-2.yml", + expectedExt: ".yml", + expectedType: files.DOCKER_COMPOSE, + }, + + // { + // name: "Can Identify Docker Compose without version", + // filePath: "./samples/valid-compose-3.yml", + // expectedExt: ".yml", + // expectedType: files.DOCKER_COMPOSE, + // }, } validExts := map[string]bool{ From 4bd385d5779ee80409137ad7d34c3f589a77b4c2 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Sun, 2 Feb 2025 12:44:57 +0530 Subject: [PATCH 27/33] chore: use schemas Signed-off-by: aabidsofi19 --- files/identification.go | 29 ++++++++++++----------------- files/tests/sanitization_test.go | 17 +++++++++-------- go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/files/identification.go b/files/identification.go index f54f644c..d4558670 100644 --- a/files/identification.go +++ b/files/identification.go @@ -10,7 +10,9 @@ import ( "strings" "github.com/layer5io/meshkit/utils/kubernetes/kompose" + "github.com/meshery/schemas/models/core" "github.com/meshery/schemas/models/v1beta1/pattern" + "gopkg.in/yaml.v3" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" @@ -28,13 +30,6 @@ import ( dockerTypes "github.com/docker/cli/cli/compose/types" ) -const MESHERY_DESIGN = "Design" -const KUBERNETES_MANIFEST = "KubernetesManifest" -const DOCKER_COMPOSE = "DockerCompose" -const KUSTOMIZATION = "Kustomization" -const HELM_CHART = "HelmChart" -const MESHERY_VIEW = "View" - type IdentifiedFile struct { Type string @@ -56,47 +51,47 @@ func IdentifyFile(sanitizedFile SanitizedFile) (IdentifiedFile, error) { // Attempt to parse the file as a Meshery design if parsed, err = ParseFileAsMesheryDesign(sanitizedFile); err == nil { return IdentifiedFile{ - Type: MESHERY_DESIGN, + Type: core.IacFileTypes.MESHERY_DESIGN, ParsedFile: parsed, }, nil } - identificationErrorsTrace[MESHERY_DESIGN] = err + identificationErrorsTrace[core.IacFileTypes.MESHERY_DESIGN] = err // Attempt to parse the file as a Kubernetes manifest if parsed, err = ParseFileAsKubernetesManifest(sanitizedFile); err == nil { return IdentifiedFile{ - Type: KUBERNETES_MANIFEST, + Type: core.IacFileTypes.KUBERNETES_MANIFEST, ParsedFile: parsed, }, nil } - identificationErrorsTrace[KUBERNETES_MANIFEST] = err + identificationErrorsTrace[core.IacFileTypes.KUBERNETES_MANIFEST] = err // Attempt to parse the file as a Helm chart if parsed, err = ParseFileAsHelmChart(sanitizedFile); err == nil { return IdentifiedFile{ - Type: HELM_CHART, + Type: core.IacFileTypes.HELM_CHART, ParsedFile: parsed, }, nil } - identificationErrorsTrace[HELM_CHART] = err + identificationErrorsTrace[core.IacFileTypes.HELM_CHART] = err // Attempt to parse the file as a Docker Compose file if parsed, err = ParseFileAsDockerCompose(sanitizedFile); err == nil { return IdentifiedFile{ - Type: DOCKER_COMPOSE, + Type: core.IacFileTypes.DOCKER_COMPOSE, ParsedFile: parsed, }, nil } - identificationErrorsTrace[DOCKER_COMPOSE] = err + identificationErrorsTrace[core.IacFileTypes.DOCKER_COMPOSE] = err // Attempt to parse the file as a Kustomization file if parsed, err = ParseFileAsKustomization(sanitizedFile); err == nil { return IdentifiedFile{ - Type: KUSTOMIZATION, + Type: core.IacFileTypes.KUSTOMIZE, ParsedFile: parsed, }, nil } - identificationErrorsTrace[KUSTOMIZATION] = err + identificationErrorsTrace[core.IacFileTypes.KUSTOMIZE] = err // If no file type matched, return a detailed error with the identification trace return IdentifiedFile{}, ErrFailedToIdentifyFile(sanitizedFile.FileName, sanitizedFile.FileExt, identificationErrorsTrace) diff --git a/files/tests/sanitization_test.go b/files/tests/sanitization_test.go index f204f832..f57c1b03 100644 --- a/files/tests/sanitization_test.go +++ b/files/tests/sanitization_test.go @@ -7,6 +7,7 @@ import ( "github.com/layer5io/meshkit/errors" "github.com/layer5io/meshkit/files" + "github.com/meshery/schemas/models/core" ) func TestSanitizeFile(t *testing.T) { @@ -72,52 +73,52 @@ func TestSanitizeFile(t *testing.T) { name: "Can Identify Design", filePath: "./samples/valid_design.yml", expectedExt: ".yml", - expectedType: files.MESHERY_DESIGN, + expectedType: core.IacFileTypes.MESHERY_DESIGN, }, { name: "Can Identify Kubernetes Manifest", filePath: "./samples/valid_manifest.yml", expectedExt: ".yml", - expectedType: files.KUBERNETES_MANIFEST, + expectedType: core.IacFileTypes.KUBERNETES_MANIFEST, }, { name: "Can Identify Kubernetes Manifest With Crds", filePath: "./samples/manifest-with-crds.yml", expectedExt: ".yml", - expectedType: files.KUBERNETES_MANIFEST, + expectedType: core.IacFileTypes.KUBERNETES_MANIFEST, }, { name: "Can Identify HelmChart", filePath: "./samples/valid-helm.tgz", expectedExt: ".tgz", - expectedType: files.HELM_CHART, + expectedType: core.IacFileTypes.HELM_CHART, }, { name: "Can Identify Kustomize archive (tar.gz)", filePath: "./samples/wordpress-kustomize.tar.gz", expectedExt: ".gz", - expectedType: files.KUSTOMIZATION, + expectedType: core.IacFileTypes.KUSTOMIZE, }, { name: "Can Identify Kustomize archive (zip)", filePath: "./samples/wordpress-kustomize.zip", expectedExt: ".zip", - expectedType: files.KUSTOMIZATION, + expectedType: core.IacFileTypes.KUSTOMIZE, }, { name: "Can Identify Docker Compose", filePath: "./samples/valid-docker-compose.yml", expectedExt: ".yml", - expectedType: files.DOCKER_COMPOSE, + expectedType: core.IacFileTypes.DOCKER_COMPOSE, }, { name: "Can Identify Docker Compose v2", filePath: "./samples/valid-compose-2.yml", expectedExt: ".yml", - expectedType: files.DOCKER_COMPOSE, + expectedType: core.IacFileTypes.DOCKER_COMPOSE, }, // { diff --git a/go.mod b/go.mod index dcabd374..f4b3a3a1 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/google/uuid v1.6.0 github.com/kubernetes/kompose v1.35.0 github.com/layer5io/meshery-operator v0.8.1 - github.com/meshery/schemas v0.7.42 + github.com/meshery/schemas v0.7.44 github.com/nats-io/nats.go v1.38.0 github.com/open-policy-agent/opa v1.0.1 github.com/opencontainers/image-spec v1.1.0 diff --git a/go.sum b/go.sum index f6561000..d5aae948 100644 --- a/go.sum +++ b/go.sum @@ -409,8 +409,8 @@ github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxU github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/meshery/schemas v0.7.42 h1:HbCt97cE/j1BxBya3UFCGFZf2jGo4J79M8EEtnDHqJo= -github.com/meshery/schemas v0.7.42/go.mod h1:ZKx5cazFfsBe9DtCjsgeDvWcTQz3MRHC+adL8n94KEE= +github.com/meshery/schemas v0.7.44 h1:LQoQDuoUDlK5CnBEEtr3J+zAnCsurTOJWaeNjCfLwck= +github.com/meshery/schemas v0.7.44/go.mod h1:xpg60NqqRXmFV7Fj5tRjDN8TofGRi/Gcbt/Ilgr06WE= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= From f1f418cb050b0cd3e0cd84461ed75b39134ac7ba Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Sun, 2 Feb 2025 20:48:22 +0530 Subject: [PATCH 28/33] add unsupported conversion err Signed-off-by: aabidsofi19 --- files/errors.go | 49 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/files/errors.go b/files/errors.go index d051bd51..29eb2075 100644 --- a/files/errors.go +++ b/files/errors.go @@ -11,19 +11,20 @@ import ( var ( // Error code - ErrUnsupportedExtensionCode = "replace_me" - ErrUnsupportedExtensionForOperationCode = "replace_me" - ErrFailedToIdentifyFileCode = "replace_me" - ErrSanitizingFileCode = "replace_me" - ErrInvalidYamlCode = "replace_me" - ErrInvalidJsonCode = "replace_me" - ErrFailedToExtractTarCode = "replace_me" - ErrUnsupportedFileTypeCode = "replace_me" - ErrInvalidKubernetesManifestCode = "replace_me" - ErrInvalidMesheryDesignCode = "replace_me" - ErrInvalidHelmChartCode = "replace_me" - ErrInvalidDockerComposeCode = "replace_me" - ErrInvalidKustomizationCode = "replace_me" + ErrUnsupportedExtensionCode = "replace_me" + ErrUnsupportedExtensionForOperationCode = "replace_me" + ErrFailedToIdentifyFileCode = "replace_me" + ErrSanitizingFileCode = "replace_me" + ErrInvalidYamlCode = "replace_me" + ErrInvalidJsonCode = "replace_me" + ErrFailedToExtractTarCode = "replace_me" + ErrUnsupportedFileTypeCode = "replace_me" + ErrInvalidKubernetesManifestCode = "replace_me" + ErrInvalidMesheryDesignCode = "replace_me" + ErrInvalidHelmChartCode = "replace_me" + ErrInvalidDockerComposeCode = "replace_me" + ErrInvalidKustomizationCode = "replace_me" + ErrFileTypeNotSupportedForDesignConversion = "replace_me" ) func ErrUnsupportedExtensionForOperation(operation string, fileName string, fileExt string, supportedExtensions []string) error { @@ -290,3 +291,25 @@ func ErrInvalidKustomization(fileName string, err error) error { return errors.New(ErrInvalidKustomizationCode, errors.Critical, sdescription, ldescription, probableCause, remedy) } + +func ErrUnsupportedFileTypeForConversionToDesign(fileName string, fileType string) error { + sdescription := []string{ + fmt.Sprintf("The file '%s' of type '%s' is not supported for conversion to a design", fileName, fileType), + } + + ldescription := []string{ + fmt.Sprintf("The file '%s' of type '%s' cannot be converted to design. Supported formats are: meshery-design, helm-chart, k8s-manifest, docker-compose.", fileName, fileType), + } + + probableCause := []string{ + "File doesn't match any supported IAC files", + "Attempting to convert a file type not enabled for design conversion", + } + + remedy := []string{ + "Verify the file format matches one of the supported types", + "Convert the file to a supported format before processing", + } + + return errors.New(ErrFileTypeNotSupportedForDesignConversion, errors.Critical, sdescription, ldescription, probableCause, remedy) +} From f89ca111855b563e1216ca7adb32e77cb8563999 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Sun, 2 Feb 2025 21:34:39 +0530 Subject: [PATCH 29/33] add support for design oci images Signed-off-by: aabidsofi19 --- files/errors.go | 29 +++++++++++++++++++ files/identification.go | 63 +++++++++++++++++++++++++++++++++++++++++ utils/error.go | 6 ++++ 3 files changed, 98 insertions(+) diff --git a/files/errors.go b/files/errors.go index 29eb2075..48c7ba67 100644 --- a/files/errors.go +++ b/files/errors.go @@ -313,3 +313,32 @@ func ErrUnsupportedFileTypeForConversionToDesign(fileName string, fileType strin return errors.New(ErrFileTypeNotSupportedForDesignConversion, errors.Critical, sdescription, ldescription, probableCause, remedy) } + +var ( + ErrNoTarInsideOCICode = "replace_me" + ErrEmptyOCIImageCode = "replace_me" + ErrUnCompressOCIArtifactCode = "replace_me" + ErrWaklingLocalDirectoryCode = "replace_me" + ErrDecodePatternCode = "replace_me" +) + +// OCI Parsing errors + +func ErrNoTarInsideOCi() error { + return errors.New(ErrNoTarInsideOCICode, errors.Alert, []string{"No tar file found inside OCI image"}, []string{"Unable to locate the compressed file(.tar.gz) inside the OCI image."}, []string{"The OCI image does not contain a ziped file."}, []string{"Verify that the OCI image contains a ziped file."}) +} +func ErrEmptyOCIImage(err error) error { + return errors.New(ErrEmptyOCIImageCode, errors.Alert, []string{}, []string{}, []string{}, []string{}) +} + +func ErrUnCompressOCIArtifact(err error) error { + return errors.New(ErrUnCompressOCIArtifactCode, errors.Alert, []string{"Failed to uncompress OCI artifact"}, []string{err.Error()}, []string{"unable to uncompress OCI artifact", "OCI artifact may be corrupted"}, []string{"check if the OCI artifact is valid and not corrupted"}) +} + +func ErrWaklingLocalDirectory(err error) error { + return errors.New(ErrWaklingLocalDirectoryCode, errors.Alert, []string{"Failed to walk local directory"}, []string{err.Error()}, []string{"unable to walk local directory", "local directory may be corrupted"}, []string{"check if the local directory is valid and not corrupted"}) +} + +func ErrDecodePattern(err error) error { + return errors.New(ErrDecodePatternCode, errors.Alert, []string{"Error failed to decode design data into go slice"}, []string{err.Error()}, []string{}, []string{}) +} diff --git a/files/identification.go b/files/identification.go index d4558670..baad12f3 100644 --- a/files/identification.go +++ b/files/identification.go @@ -9,7 +9,11 @@ import ( "path/filepath" "strings" + "github.com/layer5io/meshkit/encoding" + "github.com/layer5io/meshkit/models/oci" + "github.com/layer5io/meshkit/utils" "github.com/layer5io/meshkit/utils/kubernetes/kompose" + "github.com/layer5io/meshkit/utils/walker" "github.com/meshery/schemas/models/core" "github.com/meshery/schemas/models/v1beta1/pattern" @@ -96,6 +100,57 @@ func IdentifyFile(sanitizedFile SanitizedFile) (IdentifiedFile, error) { // If no file type matched, return a detailed error with the identification trace return IdentifiedFile{}, ErrFailedToIdentifyFile(sanitizedFile.FileName, sanitizedFile.FileExt, identificationErrorsTrace) } + +func ParseCompressedOCIArtifactIntoDesign(artifact []byte) (*pattern.PatternFile, error) { + + // Assume design is in OCI Tarball Format + tmpDir, err := oci.CreateTempOCIContentDir() + if err != nil { + return nil, utils.ErrCreateDir(err, "OCI") + } + defer os.RemoveAll(tmpDir) + + tmpInputDesignFile := filepath.Join(tmpDir, "design.tar") + file, err := os.Create(tmpInputDesignFile) + if err != nil { + return nil, utils.ErrCreateFile(err, tmpInputDesignFile) + } + defer file.Close() + + reader := bytes.NewReader(artifact) + if _, err := io.Copy(file, reader); err != nil { + return nil, utils.ErrWritingIntoFile(err, tmpInputDesignFile) + } + + tmpOutputDesignFile := filepath.Join(tmpDir, "output") + // Extract the tarball + if err := oci.UnCompressOCIArtifact(tmpInputDesignFile, tmpOutputDesignFile); err != nil { + return nil, ErrUnCompressOCIArtifact(err) + } + + files, err := walker.WalkLocalDirectory(tmpOutputDesignFile) + if err != nil { + return nil, ErrWaklingLocalDirectory(err) + } + + // TODO: Add support to merge multiple designs into one + // Currently, assumes to save only the first design + if len(files) == 0 { + return nil, ErrEmptyOCIImage(fmt.Errorf("no design file detected in the imported OCI image")) + } + design := files[0] + + var patternFile pattern.PatternFile + + err = encoding.Unmarshal([]byte(design.Content), &patternFile) + + if err != nil { + return nil, ErrDecodePattern(err) + } + patternFile.Name = design.Name + + return &patternFile, nil +} func ParseFileAsMesheryDesign(file SanitizedFile) (pattern.PatternFile, error) { var parsed pattern.PatternFile @@ -117,6 +172,14 @@ func ParseFileAsMesheryDesign(file SanitizedFile) (pattern.PatternFile, error) { err := decoder.Decode(&parsed) return parsed, err + case ".tgz", ".tar", ".tar.gz", ".zip": // try to parse oci artifacts + parsed_design, err := ParseCompressedOCIArtifactIntoDesign(file.RawData) + if parsed_design == nil || err != nil { + return pattern.PatternFile{}, err + } + + return *parsed_design, err + default: return pattern.PatternFile{}, fmt.Errorf("Invalid File extension %s", file.FileExt) } diff --git a/utils/error.go b/utils/error.go index 6c03e06b..ec4fba58 100644 --- a/utils/error.go +++ b/utils/error.go @@ -54,6 +54,8 @@ var ( // Google Sheets Service Errors ErrGoogleJwtInvalidCode = "meshkit-11279" ErrGoogleSheetSRVCode = "meshkit-11280" + + ErrWritingIntoFileCode = "replace_me" ) var ( ErrExtractType = errors.New( @@ -253,3 +255,7 @@ func ErrGoogleJwtInvalid(err error) error { func ErrGoogleSheetSRV(err error) error { return errors.New(ErrGoogleSheetSRVCode, errors.Alert, []string{"Error while creating Google Sheets Service"}, []string{err.Error()}, []string{"Issue happened with Google Sheets Service"}, []string{"Make you provide valid JWT credentials and Spreadsheet ID"}) } + +func ErrWritingIntoFile(err error, obj string) error { + return errors.New(ErrWritingIntoFileCode, errors.Alert, []string{fmt.Sprintf("failed to write into file %s", obj)}, []string{err.Error()}, []string{"Insufficient permissions to write into file", "file might be corrupted"}, []string{"check if sufficient permissions are givent to the file", "check if the file is corrupted"}) +} From 92d0e6b0ab5e0846e8cd90e03c09c60946fb8185 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Sun, 2 Feb 2025 23:03:11 +0530 Subject: [PATCH 30/33] remove old detection logic Signed-off-by: aabidsofi19 --- utils/detect_pattern_file_type.go | 110 ------------------------------ 1 file changed, 110 deletions(-) delete mode 100644 utils/detect_pattern_file_type.go diff --git a/utils/detect_pattern_file_type.go b/utils/detect_pattern_file_type.go deleted file mode 100644 index 4b36f8f1..00000000 --- a/utils/detect_pattern_file_type.go +++ /dev/null @@ -1,110 +0,0 @@ -package utils - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "encoding/json" - "io" - "regexp" - "strings" - - "cuelang.org/go/cue/errors" - "gopkg.in/yaml.v3" -) - -// Function to identify the type of input -func IdentifyInputType(data []byte) (string, error) { - if isMesheryDesign(data) { - return "Meshery Design", nil - } - if isDockerCompose(data) { - return "Docker Compose", nil - } - if isHelmChart(data) { - return "Helm Chart", nil - } - if isK8sManifest(data) { - return "Kubernetes Manifest", nil - } - return "", errors.New("unknown type") -} - -// Check if the input is a Meshery design -func isMesheryDesign(data []byte) bool { - var tempMap map[string]interface{} - - // Try unmarshaling as JSON; if it fails, try YAML - if err := json.Unmarshal(data, &tempMap); err != nil { - var yamlMap map[string]interface{} - if yaml.Unmarshal(data, &yamlMap) != nil { - return false - } - - // Convert YAML to JSON format - yamlToJSON, err := json.Marshal(yamlMap) - if err != nil { - return false - } - - // Unmarshal JSON back into tempMap - if json.Unmarshal(yamlToJSON, &tempMap) != nil { - return false - } - } - - // Check for schemaVersion key - schemaVersion, exists := tempMap["schemaVersion"].(string) - if !exists { - return false - } - - // Validate schemaVersion for Meshery Design - if strings.HasPrefix(schemaVersion, "designs.meshery.io") { - return true - } - - return false -} - -// Check if the input is a Docker Compose file -func isDockerCompose(data []byte) bool { - dockerComposeKeys := []string{"version", "services"} - content := string(data) - for _, key := range dockerComposeKeys { - if !strings.Contains(content, key+":") { - return false - } - } - return true -} - -// Check if the input is a Helm chart (.tgz file) -func isHelmChart(data []byte) bool { - gzReader, err := gzip.NewReader(bytes.NewReader(data)) - if err != nil { - return false - } - defer gzReader.Close() - - tarReader := tar.NewReader(gzReader) - for { - header, err := tarReader.Next() - if err == io.EOF { - break - } - if err != nil { - return false - } - if strings.HasSuffix(header.Name, "Chart.yaml") { - return true - } - } - return false -} - -// Check if the input is a Kubernetes manifest -func isK8sManifest(data []byte) bool { - k8sRegex := regexp.MustCompile(`(?m)^kind:\s+\w+`) - return k8sRegex.Match(data) -} From 61bbda927821a573e75f5cbc285c56dc9eae381b Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Sun, 2 Feb 2025 23:03:22 +0530 Subject: [PATCH 31/33] add extensions Signed-off-by: aabidsofi19 --- files/sanitization.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/files/sanitization.go b/files/sanitization.go index e6a11175..8116852a 100644 --- a/files/sanitization.go +++ b/files/sanitization.go @@ -25,6 +25,17 @@ type SanitizedFile struct { ExtractedContentPath string } +var ValidIacExtensions = map[string]bool{ + ".yml": true, + ".yaml": true, + ".json": true, + ".tar": true, + ".tar.gz": true, + ".tar.tgz": true, + ".zip": true, + ".gz": true, +} + func SanitizeFile(data []byte, fileName string, tempDir string, validExts map[string]bool) (SanitizedFile, error) { ext := filepath.Ext(fileName) From 0981a12d234c17409861ef317365caafe20d33fa Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Mon, 3 Feb 2025 14:02:52 +0530 Subject: [PATCH 32/33] fix design oci parsing Signed-off-by: aabidsofi19 --- files/identification.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/files/identification.go b/files/identification.go index baad12f3..7e4042f2 100644 --- a/files/identification.go +++ b/files/identification.go @@ -138,7 +138,18 @@ func ParseCompressedOCIArtifactIntoDesign(artifact []byte) (*pattern.PatternFile if len(files) == 0 { return nil, ErrEmptyOCIImage(fmt.Errorf("no design file detected in the imported OCI image")) } - design := files[0] + var design *walker.File + + // the extracted layer may contain metadata files like artifact.yml for artifacthub,etc + for _, file := range files { + if file.Name == "design.yml" { + design = file + } + } + + if design == nil { + return nil, ErrEmptyOCIImage(fmt.Errorf("No design file detected in the imported OCI image")) + } var patternFile pattern.PatternFile From 6092233c76038edaf9d8313a305004f5f3a26966 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Mon, 3 Feb 2025 14:49:44 +0530 Subject: [PATCH 33/33] remove comments Signed-off-by: aabidsofi19 --- files/conversion.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/files/conversion.go b/files/conversion.go index cf8ad393..78e32442 100644 --- a/files/conversion.go +++ b/files/conversion.go @@ -2,12 +2,7 @@ package files import ( "fmt" - - // dockerTypes "github.com/docker/cli/cli/compose/types" - "github.com/layer5io/meshkit/utils/helm" - // "github.com/layer5io/meshkit/utils/kubernetes/kompose" - // "gopkg.in/yaml.v3" "helm.sh/helm/v3/pkg/chart" )