diff --git a/cmd/cluster/clusterCreate.go b/cmd/cluster/clusterCreate.go index 8a1d1be20..8fbd28fa9 100644 --- a/cmd/cluster/clusterCreate.go +++ b/cmd/cluster/clusterCreate.go @@ -123,7 +123,7 @@ func NewCmdClusterCreate() *cobra.Command { l.Log().Fatalf("error processing/sanitizing simple config: %v", err) } - clusterConfig, err := config.TransformSimpleToClusterConfig(cmd.Context(), runtimes.SelectedRuntime, simpleCfg) + clusterConfig, err := config.TransformSimpleToClusterConfig(cmd.Context(), runtimes.SelectedRuntime, simpleCfg, configFile) if err != nil { l.Log().Fatalln(err) } diff --git a/pkg/client/cluster.go b/pkg/client/cluster.go index dacf5fa33..d061bf82d 100644 --- a/pkg/client/cluster.go +++ b/pkg/client/cluster.go @@ -247,6 +247,25 @@ func ClusterPrep(ctx context.Context, runtime k3drt.Runtime, clusterConfig *conf }) } + /* + * Step 4: Files + */ + + for id, node := range clusterConfig.Nodes { + for _, nodefile := range node.Files { + clusterConfig.Nodes[id].HookActions = append(clusterConfig.Nodes[id].HookActions, k3d.NodeHook{ + Stage: k3d.LifecycleStagePreStart, + Action: actions.WriteFileAction{ + Runtime: runtime, + Content: nodefile.Content, + Dest: nodefile.Destination, + Mode: 0644, + Description: nodefile.Description, + }, + }) + } + } + return nil } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 359733c1c..9f2687b34 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -71,6 +71,34 @@ func TestReadSimpleConfig(t *testing.T) { NodeFilters: []string{"all"}, }, }, + Files: []conf.FileWithNodeFilters{ + { + Source: "apiVersion: v1\n" + + "kind: Namespace\n" + + "metadata:\n" + + " name: foo\n", + Destination: "/var/lib/rancher/k3s/server/manifests/foo.yaml", + }, + { + Source: "apiVersion: v1\n" + + "kind: Namespace\n" + + "metadata:\n" + + " name: bar\n", + Destination: "k3s-manifests/bar.yaml", + Description: "Source: Embedded content in k3d config file, Destination: Magic shortcut path, Description: Defined", + }, + { + Source: "baz-ns.yaml", + Destination: "k3s-manifests-custom/baz.yaml", + Description: "Source: Relative path to k3d config file, Destination: Magic shortcut path, Description: Defined", + }, + { + Source: "baz-ns.yaml", + Destination: "k3s-manifests-custom/baz-server.yaml", + NodeFilters: []string{"server:*"}, + Description: "Source: Relative path to k3d config file, Destination: Magic shortcut path, Node: Defined, Description: Defined", + }, + }, Options: conf.SimpleConfigOptions{ K3dOptions: conf.SimpleConfigOptionsK3d{ Wait: true, diff --git a/pkg/config/process_test.go b/pkg/config/process_test.go index 1f6281d6a..24609bcf2 100644 --- a/pkg/config/process_test.go +++ b/pkg/config/process_test.go @@ -48,7 +48,7 @@ func TestProcessClusterConfig(t *testing.T) { t.Logf("\n========== Read Config and transform to cluster ==========\n%+v\n=================================\n", cfg) - clusterCfg, err := TransformSimpleToClusterConfig(context.Background(), runtimes.Docker, cfg.(conf.SimpleConfig)) + clusterCfg, err := TransformSimpleToClusterConfig(context.Background(), runtimes.Docker, cfg.(conf.SimpleConfig), cfgFile) if err != nil { t.Error(err) } diff --git a/pkg/config/test_assets/baz-ns.yaml b/pkg/config/test_assets/baz-ns.yaml new file mode 100644 index 000000000..b8ff1b027 --- /dev/null +++ b/pkg/config/test_assets/baz-ns.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: baz diff --git a/pkg/config/test_assets/config_test_simple.yaml b/pkg/config/test_assets/config_test_simple.yaml index af5174d35..3dd1cb884 100644 --- a/pkg/config/test_assets/config_test_simple.yaml +++ b/pkg/config/test_assets/config_test_simple.yaml @@ -23,6 +23,29 @@ env: - envVar: bar=baz nodeFilters: - all +files: + - source: | + apiVersion: v1 + kind: Namespace + metadata: + name: foo + destination: /var/lib/rancher/k3s/server/manifests/foo.yaml + # destination: "Source: Embedded content in k3d config file, Destination: Absolute path, Description: Not defined" + - source: | + apiVersion: v1 + kind: Namespace + metadata: + name: bar + destination: k3s-manifests/bar.yaml # Resolved to /var/lib/rancher/k3s/server/manifests/bar.yaml + description: 'Source: Embedded content in k3d config file, Destination: Magic shortcut path, Description: Defined' + - source: baz-ns.yaml + destination: k3s-manifests-custom/baz.yaml # Resolved to /var/lib/rancher/k3s/server/manifests/custom/bar.yaml + description: 'Source: Relative path to k3d config file, Destination: Magic shortcut path, Description: Defined' + - source: baz-ns.yaml + destination: k3s-manifests-custom/baz-server.yaml # Resolved to /var/lib/rancher/k3s/server/manifests/custom/bar-server.yaml + nodeFilters: + - "server:*" + description: 'Source: Relative path to k3d config file, Destination: Magic shortcut path, Node: Defined, Description: Defined' options: k3d: diff --git a/pkg/config/transform.go b/pkg/config/transform.go index e9aa2c203..969f9075a 100644 --- a/pkg/config/transform.go +++ b/pkg/config/transform.go @@ -47,7 +47,7 @@ import ( ) // TransformSimpleToClusterConfig transforms a simple configuration to a full-fledged cluster configuration -func TransformSimpleToClusterConfig(ctx context.Context, runtime runtimes.Runtime, simpleConfig conf.SimpleConfig) (*conf.ClusterConfig, error) { +func TransformSimpleToClusterConfig(ctx context.Context, runtime runtimes.Runtime, simpleConfig conf.SimpleConfig, configFileName string) (*conf.ClusterConfig, error) { // set default cluster name if simpleConfig.Name == "" { simpleConfig.Name = k3d.DefaultClusterName @@ -383,6 +383,35 @@ func TransformSimpleToClusterConfig(ctx context.Context, runtime runtimes.Runtim clusterCreateOpts.Registries.Config = k3sRegistry } + /* + * Files + */ + + for _, fileWithNodeFilters := range simpleConfig.Files { + nodes, err := util.FilterNodes(nodeList, fileWithNodeFilters.NodeFilters) + if err != nil { + return nil, fmt.Errorf("failed to filter nodes for file copying '%s': %w", fileWithNodeFilters, err) + } + + content, err := util.ReadFileSource(configFileName, fileWithNodeFilters.Source) + if err != nil { + return nil, fmt.Errorf("failed to read source content: %w", err) + } + + destination, err := util.ResolveFileDestination(fileWithNodeFilters.Destination) + if err != nil { + return nil, fmt.Errorf("destination path is not correct: %w", err) + } + + for _, node := range nodes { + node.Files = append(node.Files, k3d.File{ + Content: content, + Destination: destination, + Description: fileWithNodeFilters.Description, + }) + } + } + /********************** * Kubeconfig Options * **********************/ diff --git a/pkg/config/transform_test.go b/pkg/config/transform_test.go index 266b84e73..45b57729f 100644 --- a/pkg/config/transform_test.go +++ b/pkg/config/transform_test.go @@ -45,7 +45,7 @@ func TestTransformSimpleConfigToClusterConfig(t *testing.T) { t.Logf("\n========== Read Config ==========\n%+v\n=================================\n", cfg) - clusterCfg, err := TransformSimpleToClusterConfig(context.Background(), runtimes.Docker, cfg.(conf.SimpleConfig)) + clusterCfg, err := TransformSimpleToClusterConfig(context.Background(), runtimes.Docker, cfg.(conf.SimpleConfig), cfgFile) if err != nil { t.Error(err) } diff --git a/pkg/config/v1alpha5/schema.json b/pkg/config/v1alpha5/schema.json index 9c79a2365..7d53a1f5f 100644 --- a/pkg/config/v1alpha5/schema.json +++ b/pkg/config/v1alpha5/schema.json @@ -114,6 +114,28 @@ "additionalProperties": false } }, + "files": { + "type": "array", + "items": { + "type": "object", + "required": ["source", "destination"], + "properties": { + "source": { + "type": "string" + }, + "destination": { + "type": "string" + }, + "description": { + "type": "string" + }, + "nodeFilters": { + "$ref": "#/definitions/nodeFilters" + } + }, + "additionalProperties": false + } + }, "options": { "type": "object", "properties": { diff --git a/pkg/config/v1alpha5/types.go b/pkg/config/v1alpha5/types.go index b0296a1a6..a9ea26bd9 100644 --- a/pkg/config/v1alpha5/types.go +++ b/pkg/config/v1alpha5/types.go @@ -83,6 +83,13 @@ type K3sArgWithNodeFilters struct { NodeFilters []string `mapstructure:"nodeFilters" json:"nodeFilters,omitempty"` } +type FileWithNodeFilters struct { + Source string `mapstructure:"source" json:"source,omitempty"` + Destination string `mapstructure:"destination" json:"destination,omitempty"` + Description string `mapstructure:"description" json:"description,omitempty"` + NodeFilters []string `mapstructure:"nodeFilters" json:"nodeFilters,omitempty"` +} + type SimpleConfigRegistryCreateConfig struct { Name string `mapstructure:"name" json:"name,omitempty"` Host string `mapstructure:"host" json:"host,omitempty"` @@ -162,6 +169,7 @@ type SimpleConfig struct { Env []EnvVarWithNodeFilters `mapstructure:"env" json:"env,omitempty"` Registries SimpleConfigRegistries `mapstructure:"registries" json:"registries,omitempty"` HostAliases []k3d.HostAlias `mapstructure:"hostAliases" json:"hostAliases,omitempty"` + Files []FileWithNodeFilters `mapstructure:"files" json:"files,omitempty"` } // SimpleExposureOpts provides a simplified syntax compared to the original k3d.ExposureOpts diff --git a/pkg/types/files.go b/pkg/types/files.go new file mode 100644 index 000000000..32301cbdb --- /dev/null +++ b/pkg/types/files.go @@ -0,0 +1,28 @@ +/* +Copyright © 2020-2024 The k3d Author(s) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package types + +type File struct { + Content []byte `mapstructure:"content" json:"content,omitempty"` + Destination string `mapstructure:"destination" json:"destination,omitempty"` + Description string `mapstructure:"description" json:"description,omitempty"` +} diff --git a/pkg/types/types.go b/pkg/types/types.go index 0eebc73b8..4c1cf6c8c 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -291,6 +291,7 @@ type Node struct { Env []string `json:"env,omitempty"` Cmd []string // filled automatically based on role Args []string `json:"extraArgs,omitempty"` + Files []File `json:"files,omitempty"` Ports nat.PortMap `json:"portMappings,omitempty"` Restart bool `json:"restart,omitempty"` Created string `json:"created,omitempty"` diff --git a/pkg/util/files.go b/pkg/util/files.go index b5a1449fa..e363685d4 100644 --- a/pkg/util/files.go +++ b/pkg/util/files.go @@ -25,10 +25,15 @@ import ( "fmt" "os" "path" + "path/filepath" + "strings" homedir "github.com/mitchellh/go-homedir" + l "github.com/k3d-io/k3d/v5/pkg/logger" "github.com/k3d-io/k3d/v5/pkg/types" + "github.com/k3d-io/k3d/v5/pkg/types/k3s" + yaml "gopkg.in/yaml.v3" ) // GetConfigDirOrCreate will return the base path of the k3d config directory or create it if it doesn't exist yet @@ -60,3 +65,49 @@ func createDirIfNotExists(path string) error { } return nil } + +// ReadFileSource reads the file source which is either embedded in the k3d config file or relative to it. +func ReadFileSource(configFile, source string) ([]byte, error) { + sourceContent := &yaml.Node{} + sourceContent.SetString(source) + + // If the source input is embedded in the config file, use it as it is. + if sourceContent.Style == yaml.LiteralStyle || sourceContent.Style == yaml.FoldedStyle { + l.Log().Debugf("read source from embedded file with content '%s'", sourceContent.Value) + return []byte(sourceContent.Value), nil + } + + // If the source input is referenced as an external file, read its content. + sourceFilePath := filepath.Join(filepath.Dir(configFile), sourceContent.Value) + fileInfo, err := os.Stat(sourceFilePath) + if err == nil && !fileInfo.IsDir() { + fileContent, err := os.ReadFile(sourceFilePath) + if err != nil { + return nil, fmt.Errorf("cannot read file: %s", sourceFilePath) + } + l.Log().Debugf("read source from external file '%s'", sourceFilePath) + return fileContent, nil + } + + return nil, fmt.Errorf("could resolve source file path: %s", sourceFilePath) +} + +// ResolveFileDestination determines the file destination and resolves it if it has a magic shortcut. +func ResolveFileDestination(destPath string) (string, error) { + // If the destination path is absolute, then use it as it is. + if filepath.IsAbs(destPath) { + l.Log().Debugf("resolved destination with absolute path '%s'", destPath) + return destPath, nil + } + + // If the destination path has a magic shortcut, then resolve it and use it in the path. + destPathTree := strings.Split(destPath, string(os.PathSeparator)) + if shortcutPath, found := k3s.K3sPathShortcuts[destPathTree[0]]; found { + destPathTree[0] = shortcutPath + destPathResolved := filepath.Join(destPathTree...) + l.Log().Debugf("resolved destination with magic shortcut path: '%s'", destPathResolved) + return filepath.Join(destPathResolved), nil + } + + return "", fmt.Errorf("destination can be only absolute path or starts with predefined shortcut path. Could not resolve destination file path: %s", destPath) +}