Fix binary data, Add tests, Error management #91

Merged
metal3d merged 7 commits from develop into master 2024-12-03 13:44:20 +00:00
10 changed files with 241 additions and 47 deletions

View File

@@ -1,7 +1,5 @@
<div style="text-align:center; margin: auto 0 4em 0" align="center"> <div style="text-align:center; margin: auto 0 4em 0" align="center">
<img src="./doc/docs/statics/logo-vertical.svg" alt="Katenary Logo" style="max-width: 90%" align="center"/> <img src="./doc/docs/statics/logo-vertical.svg" alt="Katenary Logo" style="max-width: 90%" align="center"/>
</div> </div>
[![Documentation Status](https://readthedocs.org/projects/katenary/badge/?version=latest)](https://katenary.readthedocs.io/en/latest/?badge=latest) [![Documentation Status](https://readthedocs.org/projects/katenary/badge/?version=latest)](https://katenary.readthedocs.io/en/latest/?badge=latest)
@@ -262,7 +260,7 @@ return {
settings = { settings = {
yaml = { yaml = {
schemas = { schemas = {
["https://raw.githubusercontent.com/metal3d/katenary/refs/heads/master/katenary.json"] = "katenary.yaml", ["https://raw.githubusercontent.com/metal3d/katenary/master/katenary.json"] = "katenary.yaml",
}, },
}, },
}, },
@@ -278,12 +276,12 @@ Use this address to validate the `katenary.yaml` file in VSCode:
```json ```json
{ {
"yaml.schemas": { "yaml.schemas": {
"https://raw.githubusercontent.com/metal3d/katenary/refs/heads/master/katenary.json": "katenary.yaml" "https://raw.githubusercontent.com/metal3d/katenary/master/katenary.json": "katenary.yaml"
} }
} }
``` ```
> You can, of course, replace the `refs/heads/master` with a specific tag or branch. > You can, of course, replace the `master` with a specific tag or branch.
## What a name… ## What a name…

View File

@@ -141,11 +141,11 @@ func generateConvertCommand() *cobra.Command {
convertCmd := &cobra.Command{ convertCmd := &cobra.Command{
Use: "convert", Use: "convert",
Short: "Converts a docker-compose file to a Helm Chart", Short: "Converts a docker-compose file to a Helm Chart",
Run: func(cmd *cobra.Command, args []string) { RunE: func(cmd *cobra.Command, args []string) error {
if givenAppVersion != "" { if givenAppVersion != "" {
appVersion = &givenAppVersion appVersion = &givenAppVersion
} }
generator.Convert(generator.ConvertOptions{ return generator.Convert(generator.ConvertOptions{
Force: force, Force: force,
OutputDir: outputDir, OutputDir: outputDir,
Profiles: profiles, Profiles: profiles,

View File

@@ -119,7 +119,7 @@ type ChartTemplate struct {
``` ```
<a name="ConfigMap"></a> <a name="ConfigMap"></a>
## type [ConfigMap](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L35-L40>) ## type [ConfigMap](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L36-L41>)
ConfigMap is a kubernetes ConfigMap. Implements the DataMap interface. ConfigMap is a kubernetes ConfigMap. Implements the DataMap interface.
@@ -131,7 +131,7 @@ type ConfigMap struct {
``` ```
<a name="NewConfigMap"></a> <a name="NewConfigMap"></a>
### func [NewConfigMap](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L44>) ### func [NewConfigMap](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L45>)
```go ```go
func NewConfigMap(service types.ServiceConfig, appName string, forFile bool) *ConfigMap func NewConfigMap(service types.ServiceConfig, appName string, forFile bool) *ConfigMap
@@ -140,7 +140,7 @@ func NewConfigMap(service types.ServiceConfig, appName string, forFile bool) *Co
NewConfigMap creates a new ConfigMap from a compose service. The appName is the name of the application taken from the project name. The ConfigMap is filled by environment variables and labels "map\-env". NewConfigMap creates a new ConfigMap from a compose service. The appName is the name of the application taken from the project name. The ConfigMap is filled by environment variables and labels "map\-env".
<a name="NewConfigMapFromDirectory"></a> <a name="NewConfigMapFromDirectory"></a>
### func [NewConfigMapFromDirectory](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L117>) ### func [NewConfigMapFromDirectory](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L118>)
```go ```go
func NewConfigMapFromDirectory(service types.ServiceConfig, appName, path string) *ConfigMap func NewConfigMapFromDirectory(service types.ServiceConfig, appName, path string) *ConfigMap
@@ -148,8 +148,17 @@ func NewConfigMapFromDirectory(service types.ServiceConfig, appName, path string
NewConfigMapFromDirectory creates a new ConfigMap from a compose service. This path is the path to the file or directory. If the path is a directory, all files in the directory are added to the ConfigMap. Each subdirectory are ignored. Note that the Generate\(\) function will create the subdirectories ConfigMaps. NewConfigMapFromDirectory creates a new ConfigMap from a compose service. This path is the path to the file or directory. If the path is a directory, all files in the directory are added to the ConfigMap. Each subdirectory are ignored. Note that the Generate\(\) function will create the subdirectories ConfigMaps.
<a name="ConfigMap.AddBinaryData"></a>
### func \(\*ConfigMap\) [AddBinaryData](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L154>)
```go
func (c *ConfigMap) AddBinaryData(key string, value []byte)
```
AddBinaryData adds binary data to the configmap. Append or overwrite the value if the key already exists.
<a name="ConfigMap.AddData"></a> <a name="ConfigMap.AddData"></a>
### func \(\*ConfigMap\) [AddData](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L148>) ### func \(\*ConfigMap\) [AddData](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L149>)
```go ```go
func (c *ConfigMap) AddData(key, value string) func (c *ConfigMap) AddData(key, value string)
@@ -157,17 +166,8 @@ func (c *ConfigMap) AddData(key, value string)
AddData adds a key value pair to the configmap. Append or overwrite the value if the key already exists. AddData adds a key value pair to the configmap. Append or overwrite the value if the key already exists.
<a name="ConfigMap.AppendDir"></a>
### func \(\*ConfigMap\) [AppendDir](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L154>)
```go
func (c *ConfigMap) AppendDir(path string)
```
AddFile adds files from given path to the configmap. It is not recursive, to add all files in a directory, you need to call this function for each subdirectory.
<a name="ConfigMap.AppendFile"></a> <a name="ConfigMap.AppendFile"></a>
### func \(\*ConfigMap\) [AppendFile](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L189>) ### func \(\*ConfigMap\) [AppendFile](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L209>)
```go ```go
func (c *ConfigMap) AppendFile(path string) func (c *ConfigMap) AppendFile(path string)
@@ -175,8 +175,17 @@ func (c *ConfigMap) AppendFile(path string)
<a name="ConfigMap.AppenddDir"></a>
### func \(\*ConfigMap\) [AppenddDir](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L163>)
```go
func (c *ConfigMap) AppenddDir(path string)
```
AddFile adds files from given path to the configmap. It is not recursive, to add all files in a directory, you need to call this function for each subdirectory.
<a name="ConfigMap.Filename"></a> <a name="ConfigMap.Filename"></a>
### func \(\*ConfigMap\) [Filename](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L207>) ### func \(\*ConfigMap\) [Filename](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L232>)
```go ```go
func (c *ConfigMap) Filename() string func (c *ConfigMap) Filename() string
@@ -185,7 +194,7 @@ func (c *ConfigMap) Filename() string
Filename returns the filename of the configmap. If the configmap is used for files, the filename contains the path. Filename returns the filename of the configmap. If the configmap is used for files, the filename contains the path.
<a name="ConfigMap.SetData"></a> <a name="ConfigMap.SetData"></a>
### func \(\*ConfigMap\) [SetData](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L217>) ### func \(\*ConfigMap\) [SetData](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L242>)
```go ```go
func (c *ConfigMap) SetData(data map[string]string) func (c *ConfigMap) SetData(data map[string]string)
@@ -194,7 +203,7 @@ func (c *ConfigMap) SetData(data map[string]string)
SetData sets the data of the configmap. It replaces the entire data. SetData sets the data of the configmap. It replaces the entire data.
<a name="ConfigMap.Yaml"></a> <a name="ConfigMap.Yaml"></a>
### func \(\*ConfigMap\) [Yaml](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L222>) ### func \(\*ConfigMap\) [Yaml](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L247>)
```go ```go
func (c *ConfigMap) Yaml() ([]byte, error) func (c *ConfigMap) Yaml() ([]byte, error)
@@ -421,7 +430,7 @@ func (d *Deployment) Yaml() ([]byte, error)
Yaml returns the yaml representation of the deployment. Yaml returns the yaml representation of the deployment.
<a name="FileMapUsage"></a> <a name="FileMapUsage"></a>
## type [FileMapUsage](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L19>) ## type [FileMapUsage](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L20>)
FileMapUsage is the usage of the filemap. FileMapUsage is the usage of the filemap.

View File

@@ -196,13 +196,13 @@ type Icon string
const ( const (
IconSuccess Icon = "✅" IconSuccess Icon = "✅"
IconFailure Icon = "❌" IconFailure Icon = "❌"
IconWarning Icon = "⚠️'" IconWarning Icon = ""
IconNote Icon = "📝" IconNote Icon = "📝"
IconWorld Icon = "🌐" IconWorld Icon = "🌐"
IconPlug Icon = "🔌" IconPlug Icon = "🔌"
IconPackage Icon = "📦" IconPackage Icon = "📦"
IconCabinet Icon = "🗄️" IconCabinet Icon = "🗄️"
IconInfo Icon = "" IconInfo Icon = "🔵"
IconSecret Icon = "🔒" IconSecret Icon = "🔒"
IconConfig Icon = "🔧" IconConfig Icon = "🔧"
IconDependency Icon = "🔗" IconDependency Icon = "🔗"

View File

@@ -1,6 +1,7 @@
package generator package generator
import ( import (
"fmt"
"katenary/generator/labels" "katenary/generator/labels"
"katenary/generator/labels/labelStructs" "katenary/generator/labels/labelStructs"
"katenary/utils" "katenary/utils"
@@ -9,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"unicode/utf8"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@@ -149,58 +151,84 @@ func (c *ConfigMap) AddData(key, value string) {
c.Data[key] = value c.Data[key] = value
} }
// AddBinaryData adds binary data to the configmap. Append or overwrite the value if the key already exists.
func (c *ConfigMap) AddBinaryData(key string, value []byte) {
if c.BinaryData == nil {
c.BinaryData = make(map[string][]byte)
}
c.BinaryData[key] = value
}
// AddFile adds files from given path to the configmap. It is not recursive, to add all files in a directory, // AddFile adds files from given path to the configmap. It is not recursive, to add all files in a directory,
// you need to call this function for each subdirectory. // you need to call this function for each subdirectory.
func (c *ConfigMap) AppendDir(path string) { func (c *ConfigMap) AppendDir(path string) error {
// read all files in the path and add them to the configmap // read all files in the path and add them to the configmap
stat, err := os.Stat(path) stat, err := os.Stat(path)
if err != nil { if err != nil {
log.Fatalf("Path %s does not exist\n", path) return fmt.Errorf("Path %s does not exist, %w\n", path, err)
} }
// recursively read all files in the path and add them to the configmap // recursively read all files in the path and add them to the configmap
if stat.IsDir() { if stat.IsDir() {
files, err := os.ReadDir(path) files, err := os.ReadDir(path)
if err != nil { if err != nil {
log.Fatal(err) return err
} }
for _, file := range files { for _, file := range files {
if file.IsDir() { if file.IsDir() {
utils.Warn("Subdirectories are ignored for the moment, skipping", filepath.Join(path, file.Name()))
continue continue
} }
path := filepath.Join(path, file.Name()) path := filepath.Join(path, file.Name())
content, err := os.ReadFile(path) content, err := os.ReadFile(path)
if err != nil { if err != nil {
log.Fatal(err) return err
} }
// remove the path from the file // remove the path from the file
filename := filepath.Base(path) filename := filepath.Base(path)
c.AddData(filename, string(content)) if utf8.Valid(content) {
c.AddData(filename, string(content))
} else {
c.AddBinaryData(filename, content)
}
} }
} else { } else {
// add the file to the configmap // add the file to the configmap
content, err := os.ReadFile(path) content, err := os.ReadFile(path)
if err != nil { if err != nil {
log.Fatal(err) return err
}
filename := filepath.Base(path)
if utf8.Valid(content) {
c.AddData(filename, string(content))
} else {
c.AddBinaryData(filename, content)
} }
c.AddData(filepath.Base(path), string(content))
} }
return nil
} }
func (c *ConfigMap) AppendFile(path string) { func (c *ConfigMap) AppendFile(path string) error {
// read all files in the path and add them to the configmap // read all files in the path and add them to the configmap
stat, err := os.Stat(path) stat, err := os.Stat(path)
if err != nil { if err != nil {
log.Fatalf("Path %s does not exist\n", path) return fmt.Errorf("Path %s doesn not exists, %w", path, err)
} }
// recursively read all files in the path and add them to the configmap // recursively read all files in the path and add them to the configmap
if !stat.IsDir() { if !stat.IsDir() {
// add the file to the configmap // add the file to the configmap
content, err := os.ReadFile(path) content, err := os.ReadFile(path)
if err != nil { if err != nil {
log.Fatal(err) return err
} }
c.AddData(filepath.Base(path), string(content)) if utf8.Valid(content) {
c.AddData(filepath.Base(path), string(content))
} else {
c.AddBinaryData(filepath.Base(path), content)
}
} }
return nil
} }
// Filename returns the filename of the configmap. If the configmap is used for files, the filename contains the path. // Filename returns the filename of the configmap. If the configmap is used for files, the filename contains the path.

View File

@@ -6,6 +6,7 @@ import (
"os" "os"
"testing" "testing"
"github.com/compose-spec/compose-go/types"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
@@ -73,3 +74,19 @@ services:
t.Errorf("Expected FOO to be baz, got %s", v) t.Errorf("Expected FOO to be baz, got %s", v)
} }
} }
func TestAppendBadFile(t *testing.T) {
cm := NewConfigMap(types.ServiceConfig{}, "app", true)
err := cm.AppendFile("foo")
if err == nil {
t.Errorf("Expected error, got nil")
}
}
func TestAppendBadDir(t *testing.T) {
cm := NewConfigMap(types.ServiceConfig{}, "app", true)
err := cm.AppendDir("foo")
if err == nil {
t.Errorf("Expected error, got nil")
}
}

View File

@@ -90,7 +90,7 @@ var keyRegExp = regexp.MustCompile(`^\s*[^#]+:.*`)
// Convert a compose (docker, podman...) project to a helm chart. // Convert a compose (docker, podman...) project to a helm chart.
// It calls Generate() to generate the chart and then write it to the disk. // It calls Generate() to generate the chart and then write it to the disk.
func Convert(config ConvertOptions, dockerComposeFile ...string) { func Convert(config ConvertOptions, dockerComposeFile ...string) error {
var ( var (
templateDir = filepath.Join(config.OutputDir, "templates") templateDir = filepath.Join(config.OutputDir, "templates")
helpersPath = filepath.Join(config.OutputDir, "templates", "_helpers.tpl") helpersPath = filepath.Join(config.OutputDir, "templates", "_helpers.tpl")
@@ -105,7 +105,7 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) {
// go to the root of the project // go to the root of the project
if err := os.Chdir(filepath.Dir(dockerComposeFile[0])); err != nil { if err := os.Chdir(filepath.Dir(dockerComposeFile[0])); err != nil {
fmt.Println(utils.IconFailure, err) fmt.Println(utils.IconFailure, err)
os.Exit(1) return err
} }
defer os.Chdir(currentDir) // after the generation, go back to the original directory defer os.Chdir(currentDir) // after the generation, go back to the original directory
@@ -118,13 +118,13 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) {
project, err := parser.Parse(config.Profiles, config.EnvFiles, dockerComposeFile...) project, err := parser.Parse(config.Profiles, config.EnvFiles, dockerComposeFile...)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) return err
} }
// check older version of labels // check older version of labels
if err := checkOldLabels(project); err != nil { if err := checkOldLabels(project); err != nil {
fmt.Println(utils.IconFailure, err) fmt.Println(utils.IconFailure, err)
os.Exit(1) return err
} }
// TODO: use katenary.yaml file here to set the labels // TODO: use katenary.yaml file here to set the labels
@@ -140,7 +140,7 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) {
) )
if !overwrite { if !overwrite {
fmt.Println("Aborting") fmt.Println("Aborting")
os.Exit(126) // 126 is the exit code for "Command invoked cannot execute" return nil
} }
} }
fmt.Println() // clean line fmt.Println() // clean line
@@ -150,7 +150,7 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) {
chart, err := Generate(project) chart, err := Generate(project)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) return err
} }
// if the app version is set from the command line, use it // if the app version is set from the command line, use it
@@ -194,6 +194,7 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) {
// call helm update if needed // call helm update if needed
callHelmUpdate(config) callHelmUpdate(config)
return nil
} }
func addChartDoc(values []byte, project *types.Project) []byte { func addChartDoc(values []byte, project *types.Project) []byte {

View File

@@ -48,7 +48,9 @@ func internalCompileTest(t *testing.T, options ...string) string {
AppVersion: appVersion, AppVersion: appVersion,
ChartVersion: chartVersion, ChartVersion: chartVersion,
} }
Convert(convertOptions, "compose.yml") if err := Convert(convertOptions, "compose.yml"); err != nil {
return err.Error()
}
// launch helm lint to check the generated chart // launch helm lint to check the generated chart
if helmLint(convertOptions) != nil { if helmLint(convertOptions) != nil {

View File

@@ -2,8 +2,13 @@ package generator
import ( import (
"fmt" "fmt"
"image"
"image/color"
"image/png"
"katenary/generator/labels" "katenary/generator/labels"
"log"
"os" "os"
"path/filepath"
"testing" "testing"
v1 "k8s.io/api/apps/v1" v1 "k8s.io/api/apps/v1"
@@ -149,6 +154,140 @@ services:
} }
} }
func TestBinaryMount(t *testing.T) {
composeFile := `
services:
web:
image: nginx
volumes:
- ./images/foo.png:/var/www/foo
labels:
%[1]s/configmap-files: |-
- ./images/foo.png
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
log.Println(tmpDir)
defer teardown(tmpDir)
os.Mkdir(filepath.Join(tmpDir, "images"), 0o755)
// create a png image
pngFile := tmpDir + "/images/foo.png"
w, h := 100, 100
img := image.NewRGBA(image.Rect(0, 0, w, h))
red := color.RGBA{255, 0, 0, 255}
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
img.Set(x, y, red)
}
}
blue := color.RGBA{0, 0, 255, 255}
for y := 30; y < 70; y++ {
for x := 30; x < 70; x++ {
img.Set(x, y, blue)
}
}
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
f, err := os.Create(pngFile)
if err != nil {
t.Fatal(err)
}
png.Encode(f, img)
f.Close()
output := internalCompileTest(t, "-s", "templates/web/deployment.yaml")
d := v1.Deployment{}
yaml.Unmarshal([]byte(output), &d)
volumes := d.Spec.Template.Spec.Volumes
if len(volumes) != 1 {
t.Errorf("Expected 1 volume, got %d", len(volumes))
}
cm := corev1.ConfigMap{}
cmContent, err := helmTemplate(ConvertOptions{
OutputDir: "chart",
}, "-s", "templates/web/statics/images/configmap.yaml")
yaml.Unmarshal([]byte(cmContent), &cm)
if im, ok := cm.BinaryData["foo.png"]; !ok {
t.Errorf("Expected foo.png to be in the configmap")
} else {
if len(im) == 0 {
t.Errorf("Expected image to be non-empty")
}
}
}
func TestGloballyBinaryMount(t *testing.T) {
composeFile := `
services:
web:
image: nginx
volumes:
- ./images:/var/www/foo
labels:
%[1]s/configmap-files: |-
- ./images
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
log.Println(tmpDir)
defer teardown(tmpDir)
os.Mkdir(filepath.Join(tmpDir, "images"), 0o755)
// create a png image
pngFile := tmpDir + "/images/foo.png"
w, h := 100, 100
img := image.NewRGBA(image.Rect(0, 0, w, h))
red := color.RGBA{255, 0, 0, 255}
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
img.Set(x, y, red)
}
}
blue := color.RGBA{0, 0, 255, 255}
for y := 30; y < 70; y++ {
for x := 30; x < 70; x++ {
img.Set(x, y, blue)
}
}
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
f, err := os.Create(pngFile)
if err != nil {
t.Fatal(err)
}
png.Encode(f, img)
f.Close()
output := internalCompileTest(t, "-s", "templates/web/deployment.yaml")
d := v1.Deployment{}
yaml.Unmarshal([]byte(output), &d)
volumes := d.Spec.Template.Spec.Volumes
if len(volumes) != 1 {
t.Errorf("Expected 1 volume, got %d", len(volumes))
}
cm := corev1.ConfigMap{}
cmContent, err := helmTemplate(ConvertOptions{
OutputDir: "chart",
}, "-s", "templates/web/statics/images/configmap.yaml")
yaml.Unmarshal([]byte(cmContent), &cm)
if im, ok := cm.BinaryData["foo.png"]; !ok {
t.Errorf("Expected foo.png to be in the configmap")
} else {
if len(im) == 0 {
t.Errorf("Expected image to be non-empty")
}
}
}
func TestBindFrom(t *testing.T) { func TestBindFrom(t *testing.T) {
composeFile := ` composeFile := `
services: services:

View File

@@ -9,13 +9,13 @@ type Icon string
const ( const (
IconSuccess Icon = "✅" IconSuccess Icon = "✅"
IconFailure Icon = "❌" IconFailure Icon = "❌"
IconWarning Icon = "⚠️'" IconWarning Icon = ""
IconNote Icon = "📝" IconNote Icon = "📝"
IconWorld Icon = "🌐" IconWorld Icon = "🌐"
IconPlug Icon = "🔌" IconPlug Icon = "🔌"
IconPackage Icon = "📦" IconPackage Icon = "📦"
IconCabinet Icon = "🗄️" IconCabinet Icon = "🗄️"
IconInfo Icon = "" IconInfo Icon = "🔵"
IconSecret Icon = "🔒" IconSecret Icon = "🔒"
IconConfig Icon = "🔧" IconConfig Icon = "🔧"
IconDependency Icon = "🔗" IconDependency Icon = "🔗"